Skip to content

Commit 68500b2

Browse files
eamonnmcmanusGoogle Java Core Libraries
authored and
Google Java Core Libraries
committed
Introduce ImmutableMap.Builder.buildKeepingLast.
This method is the same as the existing `.buildOrThrow` method, except when the same key appears more than once. In that case `buildOrThrow` throws an exception, while `buildKeepingLast` uses the last value that was associated with the key. Relnotes: * A new method `ImmutableMap.Builder.buildKeepingLast()` keeps the last value for any given key rather than throwing an exception when a key appears more than once. * As a side-effect of the `buildKeepingLast()` change, the idiom `ImmutableList.copyOf(Maps.transformValues(map, function))` may produce different results if `function` has side-effects. (This is not recommended.) PiperOrigin-RevId: 416035210
1 parent c98868c commit 68500b2

File tree

18 files changed

+887
-141
lines changed

18 files changed

+887
-141
lines changed

android/guava-tests/test/com/google/common/collect/ImmutableBiMapTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -658,7 +658,8 @@ public void testDuplicateValues() {
658658
ImmutableBiMap.copyOf(map);
659659
fail();
660660
} catch (IllegalArgumentException expected) {
661-
assertThat(expected.getMessage()).contains("1");
661+
assertThat(expected.getMessage()).containsMatch("1|2");
662+
// We don't specify which of the two dups should be reported.
662663
}
663664
}
664665

android/guava-tests/test/com/google/common/collect/ImmutableMapTest.java

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@
5757
import java.util.Map;
5858
import java.util.Map.Entry;
5959
import java.util.Set;
60+
import java.util.regex.Matcher;
61+
import java.util.regex.Pattern;
6062
import junit.framework.Test;
6163
import junit.framework.TestCase;
6264
import junit.framework.TestSuite;
@@ -388,6 +390,42 @@ public void testBuilder_orderEntriesByValue_usedTwiceFails() {
388390
}
389391
}
390392

393+
@GwtIncompatible // we haven't implemented this
394+
public void testBuilder_orderEntriesByValue_keepingLast() {
395+
ImmutableMap.Builder<String, Integer> builder =
396+
new Builder<String, Integer>()
397+
.orderEntriesByValue(Ordering.natural())
398+
.put("three", 3)
399+
.put("one", 1)
400+
.put("five", 5)
401+
.put("four", 3)
402+
.put("four", 5)
403+
.put("four", 4) // this should win because it's last
404+
.put("two", 2);
405+
assertMapEquals(
406+
builder.buildKeepingLast(), "one", 1, "two", 2, "three", 3, "four", 4, "five", 5);
407+
try {
408+
builder.buildOrThrow();
409+
fail("Expected exception from duplicate keys");
410+
} catch (IllegalArgumentException expected) {
411+
}
412+
}
413+
414+
@GwtIncompatible // we haven't implemented this
415+
public void testBuilder_orderEntriesByValue_keepingLast_builderSizeFieldPreserved() {
416+
ImmutableMap.Builder<String, Integer> builder =
417+
new Builder<String, Integer>()
418+
.orderEntriesByValue(Ordering.natural())
419+
.put("one", 1)
420+
.put("one", 1);
421+
assertMapEquals(builder.buildKeepingLast(), "one", 1);
422+
try {
423+
builder.buildOrThrow();
424+
fail("Expected exception from duplicate keys");
425+
} catch (IllegalArgumentException expected) {
426+
}
427+
}
428+
391429
public void testBuilder_withImmutableEntry() {
392430
ImmutableMap<String, Integer> map =
393431
new Builder<String, Integer>().put(Maps.immutableEntry("one", 1)).buildOrThrow();
@@ -559,7 +597,7 @@ public void testPuttingTheSameKeyTwiceThrowsOnBuild() {
559597
Builder<String, Integer> builder =
560598
new Builder<String, Integer>()
561599
.put("one", 1)
562-
.put("one", 1); // throwing on this line would be even better
600+
.put("one", 1); // throwing on this line might be better but it's too late to change
563601

564602
try {
565603
builder.buildOrThrow();
@@ -568,6 +606,85 @@ public void testPuttingTheSameKeyTwiceThrowsOnBuild() {
568606
}
569607
}
570608

609+
public void testBuildKeepingLast_allowsOverwrite() {
610+
Builder<Integer, String> builder =
611+
new Builder<Integer, String>()
612+
.put(1, "un")
613+
.put(2, "deux")
614+
.put(70, "soixante-dix")
615+
.put(70, "septante")
616+
.put(70, "seventy")
617+
.put(1, "one")
618+
.put(2, "two");
619+
ImmutableMap<Integer, String> map = builder.buildKeepingLast();
620+
assertMapEquals(map, 1, "one", 2, "two", 70, "seventy");
621+
}
622+
623+
// The java7 branch has different code depending on whether the entry indexes fit in a byte,
624+
// short, or int. The small table in testBuildKeepingLast_allowsOverwrite will test the byte
625+
// case. This method tests the short case.
626+
public void testBuildKeepingLast_shortTable() {
627+
Builder<Integer, String> builder = ImmutableMap.builder();
628+
Map<Integer, String> expected = new LinkedHashMap<>();
629+
for (int i = 0; i < 1000; i++) {
630+
// Truncate to even key, so we have put(0, "0") then put(0, "1"). Half the entries are
631+
// duplicates.
632+
Integer key = i & ~1;
633+
String value = String.valueOf(i);
634+
builder.put(key, value);
635+
expected.put(key, value);
636+
}
637+
ImmutableMap<Integer, String> map = builder.buildKeepingLast();
638+
assertThat(map).hasSize(500);
639+
assertThat(map).containsExactlyEntriesIn(expected).inOrder();
640+
}
641+
642+
// This method tests the int case.
643+
public void testBuildKeepingLast_bigTable() {
644+
Builder<Integer, String> builder = ImmutableMap.builder();
645+
Map<Integer, String> expected = new LinkedHashMap<>();
646+
for (int i = 0; i < 200_000; i++) {
647+
// Truncate to even key, so we have put(0, "0") then put(0, "1"). Half the entries are
648+
// duplicates.
649+
Integer key = i & ~1;
650+
String value = String.valueOf(i);
651+
builder.put(key, value);
652+
expected.put(key, value);
653+
}
654+
ImmutableMap<Integer, String> map = builder.buildKeepingLast();
655+
assertThat(map).hasSize(100_000);
656+
assertThat(map).containsExactlyEntriesIn(expected).inOrder();
657+
}
658+
659+
@GwtIncompatible // Pattern, Matcher
660+
public void testBuilder_keepingLast_thenOrThrow() {
661+
ImmutableMap.Builder<String, Integer> builder =
662+
new Builder<String, Integer>()
663+
.put("three", 3)
664+
.put("one", 1)
665+
.put("five", 5)
666+
.put("four", 3)
667+
.put("four", 5)
668+
.put("four", 4) // this should win because it's last
669+
.put("two", 2);
670+
assertMapEquals(
671+
builder.buildKeepingLast(), "three", 3, "one", 1, "five", 5, "four", 4, "two", 2);
672+
try {
673+
builder.buildOrThrow();
674+
fail("Expected exception from duplicate keys");
675+
} catch (IllegalArgumentException expected) {
676+
// We don't really care which values the exception message contains, but they should be
677+
// different from each other. If buildKeepingLast() collapsed duplicates, that might end up
678+
// not being true.
679+
Pattern pattern =
680+
Pattern.compile("Multiple entries with same key: four=(.*) and four=(.*)");
681+
assertThat(expected).hasMessageThat().matches(pattern);
682+
Matcher matcher = pattern.matcher(expected.getMessage());
683+
assertThat(matcher.matches()).isTrue();
684+
assertThat(matcher.group(1)).isNotEqualTo(matcher.group(2));
685+
}
686+
}
687+
571688
public void testOf() {
572689
assertMapEquals(ImmutableMap.of("one", 1), "one", 1);
573690
assertMapEquals(ImmutableMap.of("one", 1, "two", 2), "one", 1, "two", 2);

android/guava/src/com/google/common/collect/ImmutableBiMap.java

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -448,10 +448,30 @@ public ImmutableBiMap<K, V> buildOrThrow() {
448448
if (size == 0) {
449449
return of();
450450
}
451-
sortEntries();
451+
if (valueComparator != null) {
452+
if (entriesUsed) {
453+
alternatingKeysAndValues = Arrays.copyOf(alternatingKeysAndValues, 2 * size);
454+
}
455+
sortEntries(alternatingKeysAndValues, size, valueComparator);
456+
}
452457
entriesUsed = true;
453458
return new RegularImmutableBiMap<K, V>(alternatingKeysAndValues, size);
454459
}
460+
461+
/**
462+
* Throws {@link UnsupportedOperationException}. This method is inherited from {@link
463+
* ImmutableMap.Builder}, but it does not make sense for bimaps.
464+
*
465+
* @throws UnsupportedOperationException always
466+
* @deprecated This method does not make sense for bimaps and should not be called.
467+
* @since NEXT
468+
*/
469+
@DoNotCall
470+
@Deprecated
471+
@Override
472+
public ImmutableBiMap<K, V> buildKeepingLast() {
473+
throw new UnsupportedOperationException("Not supported for bimaps");
474+
}
455475
}
456476

457477
/**

android/guava/src/com/google/common/collect/ImmutableMap.java

Lines changed: 121 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,15 @@
3333
import java.io.Serializable;
3434
import java.util.AbstractMap;
3535
import java.util.Arrays;
36+
import java.util.BitSet;
3637
import java.util.Collection;
3738
import java.util.Collections;
3839
import java.util.Comparator;
40+
import java.util.HashSet;
3941
import java.util.Iterator;
4042
import java.util.Map;
4143
import java.util.Map.Entry;
44+
import java.util.Set;
4245
import java.util.SortedMap;
4346
import javax.annotation.CheckForNull;
4447
import org.checkerframework.checker.nullness.qual.Nullable;
@@ -337,7 +340,7 @@ public static <K, V> Builder<K, V> builderWithExpectedSize(int expectedSize) {
337340
}
338341

339342
static void checkNoConflict(
340-
boolean safe, String conflictDescription, Entry<?, ?> entry1, Entry<?, ?> entry2) {
343+
boolean safe, String conflictDescription, Object entry1, Object entry2) {
341344
if (!safe) {
342345
throw conflictException(conflictDescription, entry1, entry2);
343346
}
@@ -384,6 +387,11 @@ public static class Builder<K, V> {
384387
@Nullable Object[] alternatingKeysAndValues;
385388
int size;
386389
boolean entriesUsed;
390+
/**
391+
* If non-null, a duplicate key we found in a previous buildKeepingLast() or buildOrThrow()
392+
* call. A later buildOrThrow() can simply report this duplicate immediately.
393+
*/
394+
@Nullable DuplicateKey duplicateKey;
387395

388396
/**
389397
* Creates a new builder. The returned builder is equivalent to the builder generated by {@link
@@ -498,10 +506,46 @@ Builder<K, V> combine(Builder<K, V> other) {
498506
return this;
499507
}
500508

501-
/*
502-
* TODO(kevinb): Should build() and the ImmutableBiMap & ImmutableSortedMap
503-
* versions throw an IllegalStateException instead?
504-
*/
509+
private ImmutableMap<K, V> build(boolean throwIfDuplicateKeys) {
510+
if (throwIfDuplicateKeys && duplicateKey != null) {
511+
throw duplicateKey.exception();
512+
}
513+
/*
514+
* If entries is full, then this implementation may end up using the entries array
515+
* directly and writing over the entry objects with non-terminal entries, but this is
516+
* safe; if this Builder is used further, it will grow the entries array (so it can't
517+
* affect the original array), and future build() calls will always copy any entry
518+
* objects that cannot be safely reused.
519+
*/
520+
// localAlternatingKeysAndValues is an alias for the alternatingKeysAndValues field, except if
521+
// we end up removing duplicates in a copy of the array.
522+
@Nullable Object[] localAlternatingKeysAndValues;
523+
int localSize = size;
524+
if (valueComparator == null) {
525+
localAlternatingKeysAndValues = alternatingKeysAndValues;
526+
} else {
527+
if (entriesUsed) {
528+
alternatingKeysAndValues = Arrays.copyOf(alternatingKeysAndValues, 2 * size);
529+
}
530+
localAlternatingKeysAndValues = alternatingKeysAndValues;
531+
if (!throwIfDuplicateKeys) {
532+
// We want to retain only the last-put value for any given key, before sorting.
533+
// This could be improved, but orderEntriesByValue is rather rarely used anyway.
534+
localAlternatingKeysAndValues = lastEntryForEachKey(localAlternatingKeysAndValues, size);
535+
if (localAlternatingKeysAndValues.length < alternatingKeysAndValues.length) {
536+
localSize = localAlternatingKeysAndValues.length >>> 1;
537+
}
538+
}
539+
sortEntries(localAlternatingKeysAndValues, localSize, valueComparator);
540+
}
541+
entriesUsed = true;
542+
ImmutableMap<K, V> map =
543+
RegularImmutableMap.create(localSize, localAlternatingKeysAndValues, this);
544+
if (throwIfDuplicateKeys && duplicateKey != null) {
545+
throw duplicateKey.exception();
546+
}
547+
return map;
548+
}
505549

506550
/**
507551
* Returns a newly-created immutable map. The iteration order of the returned map is the order
@@ -527,40 +571,84 @@ public ImmutableMap<K, V> build() {
527571
* @throws IllegalArgumentException if duplicate keys were added
528572
* @since 31.0
529573
*/
530-
@SuppressWarnings("unchecked")
531574
public ImmutableMap<K, V> buildOrThrow() {
532-
/*
533-
* If entries is full, then this implementation may end up using the entries array
534-
* directly and writing over the entry objects with non-terminal entries, but this is
535-
* safe; if this Builder is used further, it will grow the entries array (so it can't
536-
* affect the original array), and future build() calls will always copy any entry
537-
* objects that cannot be safely reused.
538-
*/
539-
sortEntries();
540-
entriesUsed = true;
541-
return RegularImmutableMap.create(size, alternatingKeysAndValues);
575+
return build(true);
542576
}
543577

544-
void sortEntries() {
545-
if (valueComparator != null) {
546-
if (entriesUsed) {
547-
alternatingKeysAndValues = Arrays.copyOf(alternatingKeysAndValues, 2 * size);
548-
}
549-
Entry<K, V>[] entries = new Entry[size];
550-
for (int i = 0; i < size; i++) {
551-
// requireNonNull is safe because the first `2*size` elements have been filled in.
552-
entries[i] =
553-
new AbstractMap.SimpleImmutableEntry<K, V>(
554-
(K) requireNonNull(alternatingKeysAndValues[2 * i]),
555-
(V) requireNonNull(alternatingKeysAndValues[2 * i + 1]));
578+
/**
579+
* Returns a newly-created immutable map, using the last value for any key that was added more
580+
* than once. The iteration order of the returned map is the order in which entries were
581+
* inserted into the builder, unless {@link #orderEntriesByValue} was called, in which case
582+
* entries are sorted by value. If a key was added more than once, it appears in iteration order
583+
* based on the first time it was added, again unless {@link #orderEntriesByValue} was called.
584+
*
585+
* @since NEXT
586+
*/
587+
public ImmutableMap<K, V> buildKeepingLast() {
588+
return build(false);
589+
}
590+
591+
static <V> void sortEntries(
592+
@Nullable Object[] alternatingKeysAndValues, int size, Comparator<V> valueComparator) {
593+
@SuppressWarnings({"rawtypes", "unchecked"})
594+
Entry<Object, V>[] entries = new Entry[size];
595+
for (int i = 0; i < size; i++) {
596+
// requireNonNull is safe because the first `2*size` elements have been filled in.
597+
Object key = requireNonNull(alternatingKeysAndValues[2 * i]);
598+
@SuppressWarnings("unchecked")
599+
V value = (V) requireNonNull(alternatingKeysAndValues[2 * i + 1]);
600+
entries[i] = new AbstractMap.SimpleImmutableEntry<Object, V>(key, value);
601+
}
602+
Arrays.sort(
603+
entries, 0, size, Ordering.from(valueComparator).onResultOf(Maps.<V>valueFunction()));
604+
for (int i = 0; i < size; i++) {
605+
alternatingKeysAndValues[2 * i] = entries[i].getKey();
606+
alternatingKeysAndValues[2 * i + 1] = entries[i].getValue();
607+
}
608+
}
609+
610+
private @Nullable Object[] lastEntryForEachKey(
611+
@Nullable Object[] localAlternatingKeysAndValues, int size) {
612+
Set<Object> seenKeys = new HashSet<>();
613+
BitSet dups = new BitSet(); // slots that are overridden by a later duplicate key
614+
for (int i = size - 1; i >= 0; i--) {
615+
Object key = requireNonNull(localAlternatingKeysAndValues[2 * i]);
616+
if (!seenKeys.add(key)) {
617+
dups.set(i);
556618
}
557-
Arrays.sort(
558-
entries, 0, size, Ordering.from(valueComparator).onResultOf(Maps.<V>valueFunction()));
559-
for (int i = 0; i < size; i++) {
560-
alternatingKeysAndValues[2 * i] = entries[i].getKey();
561-
alternatingKeysAndValues[2 * i + 1] = entries[i].getValue();
619+
}
620+
if (dups.isEmpty()) {
621+
return localAlternatingKeysAndValues;
622+
}
623+
Object[] newAlternatingKeysAndValues = new Object[(size - dups.cardinality()) * 2];
624+
for (int inI = 0, outI = 0; inI < size * 2; ) {
625+
if (dups.get(inI >>> 1)) {
626+
inI += 2;
627+
} else {
628+
newAlternatingKeysAndValues[outI++] =
629+
requireNonNull(localAlternatingKeysAndValues[inI++]);
630+
newAlternatingKeysAndValues[outI++] =
631+
requireNonNull(localAlternatingKeysAndValues[inI++]);
562632
}
563633
}
634+
return newAlternatingKeysAndValues;
635+
}
636+
637+
static final class DuplicateKey {
638+
private final Object key;
639+
private final Object value1;
640+
private final Object value2;
641+
642+
DuplicateKey(Object key, Object value1, Object value2) {
643+
this.key = key;
644+
this.value1 = value1;
645+
this.value2 = value2;
646+
}
647+
648+
IllegalArgumentException exception() {
649+
return new IllegalArgumentException(
650+
"Multiple entries with same key: " + key + "=" + value1 + " and " + key + "=" + value2);
651+
}
564652
}
565653
}
566654

0 commit comments

Comments
 (0)