diff --git a/guava-gwt/src-super/com/google/common/collect/super/com/google/common/collect/ImmutableMap.java b/guava-gwt/src-super/com/google/common/collect/super/com/google/common/collect/ImmutableMap.java index 07101ef2860e..348acc7f24a0 100644 --- a/guava-gwt/src-super/com/google/common/collect/super/com/google/common/collect/ImmutableMap.java +++ b/guava-gwt/src-super/com/google/common/collect/super/com/google/common/collect/ImmutableMap.java @@ -23,15 +23,19 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.io.Serializable; +import java.util.Arrays; +import java.util.BitSet; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.EnumMap; +import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Set; import java.util.function.BinaryOperator; import java.util.function.Function; import java.util.stream.Collector; @@ -406,6 +410,54 @@ public static ImmutableMap copyOf( } } + public static , V> ImmutableMap sortedCopyOf( + Map map) { + return sortedCopyOf(Ordering.natural(), map); + } + + public static ImmutableMap sortedCopyOf( + Comparator keyComparator, Map map) { + checkNotNull(keyComparator); + @SuppressWarnings({"unchecked", "rawtypes"}) // entries are non-null + Entry[] entryArray = map.entrySet().toArray((Entry[]) new Entry[map.size()]); + switch (entryArray.length) { + case 0: + return of(); + case 1: + Entry onlyEntry = entryArray[0]; + return of(onlyEntry.getKey(), onlyEntry.getValue()); + default: + @SuppressWarnings("nullness") // entries 0..entryArray.length-1 are non-null + Entry[] nonNullEntries = entryArray; + Entry[] lastEntryForEachKey = lastEntryForEachKey(nonNullEntries, entryArray.length); + Entry[] toSort = (lastEntryForEachKey == null) ? nonNullEntries : lastEntryForEachKey; + Arrays.sort(toSort, Ordering.from(keyComparator).onResultOf(Entry::getKey)); + return new RegularImmutableMap(/* throwIfDuplicateKeys= */ false, toSort); + } + } + + private static Entry @Nullable [] lastEntryForEachKey( + Entry[] entries, int size) { + Set seen = new HashSet(); + BitSet dups = new BitSet(); // slots that are overridden by a later duplicate key + for (int i = size - 1; i>= 0; i--) { + if (!seen.add(entries[i].getKey())) { + dups.set(i); + } + } + if (dups.isEmpty()) { + return null; + } + @SuppressWarnings({"rawtypes", "unchecked"}) + Entry[] newEntries = new Entry[size - dups.cardinality()]; + for (int inI = 0, outI = 0; inI < size; inI++) { + if (!dups.get(inI)) { + newEntries[outI++] = entries[inI]; + } + } + return newEntries; + } + abstract boolean isPartialView(); @Override diff --git a/guava-tests/test/com/google/common/collect/ImmutableMapTest.java b/guava-tests/test/com/google/common/collect/ImmutableMapTest.java index 331d7f0811cd..62fe9002dd9b 100644 --- a/guava-tests/test/com/google/common/collect/ImmutableMapTest.java +++ b/guava-tests/test/com/google/common/collect/ImmutableMapTest.java @@ -55,10 +55,12 @@ import java.io.ObjectOutputStream; import java.io.Serializable; import java.util.AbstractMap; +import java.util.AbstractSet; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.EnumMap; +import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -784,6 +786,83 @@ public void testCopyOf() { assertThat(ImmutableMap.copyOf(copy)).isSameInstanceAs(copy); } + public void testSortedCopyOf_natural() { + Map map = new LinkedHashMap(); + map.put("banana", 3); + map.put("apple", 1); + map.put("cherry", 4); + map.put("avocado", 2); + ImmutableMap copy = ImmutableMap.sortedCopyOf(map); + assertMapEquals(copy, "apple", 1, "avocado", 2, "banana", 3, "cherry", 4); + } + + public void testSortedCopyOf_natural_empty() { + ImmutableMap copy = + ImmutableMap.sortedCopyOf(Collections.emptyMap()); + assertThat(copy).isEmpty(); + } + + public void testSortedCopyOf_natural_singleton() { + ImmutableMap copy = ImmutableMap.sortedCopyOf(singletonMap("one", 1)); + assertMapEquals(copy, "one", 1); + } + + public void testSortedCopyOf_natural_containsNullValue() { + Map map = new LinkedHashMap(); + map.put("one", 1); + map.put("two", null); + assertThrows(NullPointerException.class, () -> ImmutableMap.sortedCopyOf(map)); + } + + public void testSortedCopyOf_natural_singleton_containsNullValue() { + Map map = new LinkedHashMap(); + map.put("one", null); + assertThrows(NullPointerException.class, () -> ImmutableMap.sortedCopyOf(map)); + } + + public void testSortedCopyOf_comparator() { + Map map = new LinkedHashMap(); + map.put("a", 1); + map.put("b", 2); + map.put("A", 3); + map.put("c", 4); + ImmutableMap copy = + ImmutableMap.sortedCopyOf(String.CASE_INSENSITIVE_ORDER, map); + assertMapEquals(copy, "a", 1, "A", 3, "b", 2, "c", 4); + } + + public void testSortedCopyOf_comparator_empty() { + ImmutableMap copy = + ImmutableMap.sortedCopyOf( + String.CASE_INSENSITIVE_ORDER, Collections.emptyMap()); + assertThat(copy).isEmpty(); + } + + public void testSortedCopyOf_comparator_singleton() { + ImmutableMap copy = + ImmutableMap.sortedCopyOf(String.CASE_INSENSITIVE_ORDER, singletonMap("a", 1)); + assertMapEquals(copy, "a", 1); + } + + public void testSortedCopyOf_comparator_containsNullValue() { + Map map = new LinkedHashMap(); + map.put("one", 1); + map.put("two", null); + assertThrows( + NullPointerException.class, + () -> ImmutableMap.sortedCopyOf(String.CASE_INSENSITIVE_ORDER, map)); + } + + public void testSortedCopyOf_duplicateKeys_keepLast() { + assertMapEquals(ImmutableMap.sortedCopyOf(mapWithDuplicateKeys()), "a", 4, "b", 3); + } + + public void testSortedCopyOf_comparator_duplicateKeys_keepLast() { + assertMapEquals( + ImmutableMap.sortedCopyOf(String.CASE_INSENSITIVE_ORDER, mapWithDuplicateKeys()), + "a", 4, "b", 3); + } + public void testToImmutableMap() { Collector, ?, ImmutableMap> collector = toImmutableMap(Entry::getKey, Entry::getValue); @@ -859,6 +938,36 @@ private static void assertMapEquals(Map map, Object... alternatingK assertThat(map).containsExactlyEntriesIn(expected).inOrder(); } + /** + * Returns a map whose {@code entrySet()} returns entries that contain duplicate keys. This is not + * a well-behaved map, but it lets us test {@link ImmutableMap#sortedCopyOf}'s handling of + * duplicate keys. + */ + private static Map mapWithDuplicateKeys() { + List> entries = + asList( + immutableEntry("b", 1), + immutableEntry("a", 2), + immutableEntry("b", 3), + immutableEntry("a", 4)); + return new AbstractMap() { + @Override + public Set> entrySet() { + return new AbstractSet>() { + @Override + public Iterator> iterator() { + return entries.iterator(); + } + + @Override + public int size() { + return entries.size(); + } + }; + } + }; + } + private static class IntHolder implements Serializable { private int value; diff --git a/guava/src/com/google/common/collect/ImmutableMap.java b/guava/src/com/google/common/collect/ImmutableMap.java index b64fcd80adf7..5b2a1b6af65c 100644 --- a/guava/src/com/google/common/collect/ImmutableMap.java +++ b/guava/src/com/google/common/collect/ImmutableMap.java @@ -645,33 +645,6 @@ ImmutableMap buildJdkBacked() { } } - /** - * Scans the first {@code size} elements of {@code entries} looking for duplicate keys. If - * duplicates are found, a new correctly-sized array is returned with the same elements (up to - * {@code size}), except containing only the last occurrence of each duplicate key. Otherwise - * {@code null} is returned. - */ - private static Entry @Nullable [] lastEntryForEachKey( - Entry[] entries, int size) { - Set seen = new HashSet(); - BitSet dups = new BitSet(); // slots that are overridden by a later duplicate key - for (int i = size - 1; i>= 0; i--) { - if (!seen.add(entries[i].getKey())) { - dups.set(i); - } - } - if (dups.isEmpty()) { - return null; - } - @SuppressWarnings({"rawtypes", "unchecked"}) - Entry[] newEntries = new Entry[size - dups.cardinality()]; - for (int inI = 0, outI = 0; inI < size; inI++) { - if (!dups.get(inI)) { - newEntries[outI++] = entries[inI]; - } - } - return newEntries; - } } /** @@ -742,6 +715,111 @@ public static ImmutableMap copyOf( } } + /** + * Returns an immutable map containing the same entries as {@code map}, sorted according to the + * natural ordering of the keys. The sorting algorithm used is stable, so entries whose keys + * compare as equal will stay in the order in which they appear in the input. In addition, if two + * or more entries have keys that are equal, the value associated with the last such entry + * is kept. + * + *

If you want a map whose entries are always sorted, even under further iteration, use {@link + * ImmutableSortedMap} instead: In exchange for {@code O(log N)} {@link Map#get} lookups (instead + * of the {@code O(1)} lookups of the map returned by this method), {@code ImmutableSortedMap} + * guarantees that its {@code entrySet()}, {@code keySet()}, and {@code values()} views iterate in + * sorted order forever. + * + *

Java 8+ users: If you want to convert a {@link java.util.stream.Stream} of {@link + * Entry} instances to a sorted {@code ImmutableMap}, use {@code + * stream.sorted(Map.Entry.comparingByKey()).collect(toImmutableMap(Entry::getKey, + * Entry::getValue))}. (Unlike this method, that {@code Collector} does not tolerate duplicate + * keys.) + * + * @throws NullPointerException if any key or value in {@code map} is null + * @since 33.7.0 + */ + public static , V> ImmutableMap sortedCopyOf( + Map map) { + return sortedCopyOf(Ordering.natural(), map); + } + + /** + * Returns an immutable map containing the same entries as {@code map}, sorted according to the + * specified {@code Comparator} applied to the keys. The sorting algorithm used is stable, so + * entries whose keys compare as equal will stay in the order in which they appear in the input. + * In addition, if two or more entries have keys that are equal, the value associated with the + * last such entry is kept. + * + *

If you want a map whose entries are always sorted, even under further iteration, use {@link + * ImmutableSortedMap} instead: In exchange for {@code O(log N)} {@link Map#get} lookups (instead + * of the {@code O(1)} lookups of the map returned by this method), {@code ImmutableSortedMap} + * guarantees that its {@code entrySet()}, {@code keySet()}, and {@code values()} views iterate in + * sorted order forever. + * + *

Java 8+ users: If you want to convert a {@link java.util.stream.Stream} of {@link + * Entry} instances to a sorted {@code ImmutableMap}, use {@code + * stream.sorted(comparator).collect(toImmutableMap(Entry::getKey, Entry::getValue))}. (Unlike + * this method, that {@code Collector} does not tolerate duplicate keys.) + * + * @throws NullPointerException if {@code keyComparator} is null, or if any key or value in {@code + * map} is null + * @since 33.7.0 + */ + public static ImmutableMap sortedCopyOf( + Comparator keyComparator, Map map) { + checkNotNull(keyComparator); + @SuppressWarnings("unchecked") // we'll only be using getKey and getValue, which are covariant + Entry[] entryArray = (Entry[]) map.entrySet().toArray(EMPTY_ENTRY_ARRAY); + switch (entryArray.length) { + case 0: + return of(); + case 1: + // requireNonNull is safe because the first `size` elements have been filled in. + Entry onlyEntry = requireNonNull(entryArray[0]); + return of(onlyEntry.getKey(), onlyEntry.getValue()); + default: + /* + * We want to retain only the last value for any given key, before sorting. This gives us + * well-defined behavior for duplicate keys, even when the keyComparator is not consistent + * with equals. + */ + @SuppressWarnings("nullness") // entries 0..entryArray.length-1 are non-null + Entry[] nonNullEntries = entryArray; + Entry[] lastEntryForEachKey = lastEntryForEachKey(nonNullEntries, entryArray.length); + Entry[] toSort = (lastEntryForEachKey == null) ? nonNullEntries : lastEntryForEachKey; + int size = toSort.length; + sort(toSort, 0, size, Ordering.from(keyComparator).onResultOf(Entry::getKey)); + return RegularImmutableMap.fromEntryArray(size, toSort, /* throwIfDuplicateKeys= */ false); + } + } + + /** + * Scans the first {@code size} elements of {@code entries} looking for duplicate keys. If + * duplicates are found, a new correctly-sized array is returned with the same elements (up to + * {@code size}), except containing only the last occurrence of each duplicate key. Otherwise + * {@code null} is returned. + */ + private static Entry @Nullable [] lastEntryForEachKey( + Entry[] entries, int size) { + Set seen = new HashSet(); + BitSet dups = new BitSet(); // slots that are overridden by a later duplicate key + for (int i = size - 1; i>= 0; i--) { + if (!seen.add(entries[i].getKey())) { + dups.set(i); + } + } + if (dups.isEmpty()) { + return null; + } + @SuppressWarnings({"rawtypes", "unchecked"}) + Entry[] newEntries = new Entry[size - dups.cardinality()]; + for (int inI = 0, outI = 0; inI < size; inI++) { + if (!dups.get(inI)) { + newEntries[outI++] = entries[inI]; + } + } + return newEntries; + } + static final Entry[] EMPTY_ENTRY_ARRAY = new Entry[0]; abstract static class IteratorBasedImmutableMap extends ImmutableMap {

AltStyle によって変換されたページ (->オリジナル) /