From fd6a3c07545adb22fba41f2946e6c9e903664efe Mon Sep 17 00:00:00 2001 From: DerrickUnleashed Date: Tue, 4 Aug 2026 16:36:52 +0530 Subject: [PATCH 1/3] Add `ImmutableMap.sortedCopyOf`. Returns an immutable map that iterates in sorted key order while still using hashing for lookups, as an alternative to ImmutableSortedMap's O(log N) lookups. The sort is stable, and duplicate keys are resolved in favor of the last value, matching buildKeepingLast(). RELNOTES=`collect`: Added `ImmutableMap.sortedCopyOf`, which returns an immutable map that is sorted by key. --- .../google/common/collect/ImmutableMap.java | 132 ++++++++++++++---- 1 file changed, 105 insertions(+), 27 deletions(-) diff --git a/guava/src/com/google/common/collect/ImmutableMap.java b/guava/src/com/google/common/collect/ImmutableMap.java index 40e1b99043e3..21f186459f7d 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 { From 9e8217ea8a8c249020cc299a8f2bca8882239133 Mon Sep 17 00:00:00 2001 From: DerrickUnleashed Date: Tue, 4 Aug 2026 16:36:58 +0530 Subject: [PATCH 2/3] Add GWT super-source implementation of `ImmutableMap.sortedCopyOf`. RELNOTES=n/a --- .../google/common/collect/ImmutableMap.java | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) 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 From 07ae388a1204c1846f5e217e104fc8cf0e0a03e8 Mon Sep 17 00:00:00 2001 From: DerrickUnleashed Date: Tue, 4 Aug 2026 16:37:08 +0530 Subject: [PATCH 3/3] Add tests for `ImmutableMap.sortedCopyOf`. Covers natural and comparator ordering, stability, null handling, and duplicate-key behavior (last value wins). RELNOTES=n/a --- .../common/collect/ImmutableMapTest.java | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) 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;

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