From 6d93913af9a67caa0b95808bf5ccced44f8040fc Mon Sep 17 00:00:00 2001 From: Nickita Khylkouski <90287684+nickita-khylkouski@users.noreply.github.com> Date: Wed, 28 Jan 2026 11:54:54 -0800 Subject: [PATCH] Preserve spliterator characteristics in collection views. Fix CollectSpliterators.filter() to preserve IMMUTABLE and CONCURRENT characteristics from the source spliterator, matching JDK Stream.filter() behavior which only clears SIZED. Add spliterator() overrides to Lists inner classes (OnePlusArrayList, TwoPlusArrayList, CharSequenceAsList, AbstractListWrapper) and Maps inner classes (KeySet, Values) so that collection views properly report their characteristics rather than falling back to AbstractList defaults. Fixes https://github.com/google/guava/issues/8165 --- .../collect/CollectSpliteratorsTest.java | 27 ++++++++ .../com/google/common/collect/ListsTest.java | 65 +++++++++++++++++++ .../com/google/common/collect/MapsTest.java | 28 ++++++++ .../common/collect/CollectSpliterators.java | 6 +- .../src/com/google/common/collect/Lists.java | 32 +++++++++ guava/src/com/google/common/collect/Maps.java | 15 +++++ 6 files changed, 172 insertions(+), 1 deletion(-) diff --git a/guava-tests/test/com/google/common/collect/CollectSpliteratorsTest.java b/guava-tests/test/com/google/common/collect/CollectSpliteratorsTest.java index 7e49123e11bc..40e666d7a7b5 100644 --- a/guava-tests/test/com/google/common/collect/CollectSpliteratorsTest.java +++ b/guava-tests/test/com/google/common/collect/CollectSpliteratorsTest.java @@ -25,6 +25,7 @@ import java.util.Arrays; import java.util.List; import java.util.Spliterator; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.stream.DoubleStream; import java.util.stream.IntStream; import java.util.stream.LongStream; @@ -101,6 +102,32 @@ public void testFlatMapToDouble_nullStream() { .expect(1.0, 1.0, 2.0, 3.0); } + @GwtIncompatible // CopyOnWriteArrayList + public void testFilterPreservesImmutableCharacteristic() { + CopyOnWriteArrayList cow = new CopyOnWriteArrayList<>(Arrays.asList("a", "b", "c")); + Spliterator source = cow.spliterator(); + assertTrue((source.characteristics() & Spliterator.IMMUTABLE) != 0); + + Spliterator filtered = + CollectSpliterators.filter(cow.spliterator(), s -> !s.equals("b")); + assertTrue( + "filter() should preserve IMMUTABLE from source", + (filtered.characteristics() & Spliterator.IMMUTABLE) != 0); + } + + @GwtIncompatible + public void testFilterDropsSized() { + Spliterator source = + Arrays.spliterator(new String[] {"a", "b", "c"}); + assertTrue((source.characteristics() & Spliterator.SIZED) != 0); + + Spliterator filtered = + CollectSpliterators.filter(source, s -> !s.equals("b")); + assertFalse( + "filter() should not preserve SIZED", + (filtered.characteristics() & Spliterator.SIZED) != 0); + } + public void testMultisetsSpliterator() { Multiset multiset = TreeMultiset.create(); multiset.add("a", 3); diff --git a/guava-tests/test/com/google/common/collect/ListsTest.java b/guava-tests/test/com/google/common/collect/ListsTest.java index bd46674a18d2..7686c55cccf9 100644 --- a/guava-tests/test/com/google/common/collect/ListsTest.java +++ b/guava-tests/test/com/google/common/collect/ListsTest.java @@ -57,6 +57,7 @@ import java.util.ListIterator; import java.util.NoSuchElementException; import java.util.RandomAccess; +import java.util.Spliterator; import java.util.concurrent.CopyOnWriteArrayList; import junit.framework.Test; import junit.framework.TestCase; @@ -998,4 +999,68 @@ public void testPartitionSize_1() { public void testPartitionSize_2() { assertEquals(2, partition(nCopies(0x40000001, 1), 0x40000000).size()); } + + @GwtIncompatible // Spliterator + @J2ktIncompatible + public void testAsListSpliteratorIsImmutable() { + List list = Lists.asList("a", new String[] {"b", "c"}); + int characteristics = list.spliterator().characteristics(); + assertTrue("asList should report IMMUTABLE", (characteristics & Spliterator.IMMUTABLE) != 0); + assertTrue("asList should report ORDERED", (characteristics & Spliterator.ORDERED) != 0); + assertTrue("asList should report SIZED", (characteristics & Spliterator.SIZED) != 0); + } + + @GwtIncompatible // Spliterator + @J2ktIncompatible + public void testAsListSpliteratorIteration() { + List list = Lists.asList("a", new String[] {"b", "c"}); + List collected = new ArrayList<>(); + list.spliterator().forEachRemaining(collected::add); + assertEquals(asList("a", "b", "c"), collected); + } + + @GwtIncompatible // Spliterator + @J2ktIncompatible + public void testAsListTwoSpliteratorIsImmutable() { + List list = Lists.asList("a", "b", new String[] {"c"}); + int characteristics = list.spliterator().characteristics(); + assertTrue( + "asList(2+) should report IMMUTABLE", (characteristics & Spliterator.IMMUTABLE) != 0); + } + + @GwtIncompatible // Spliterator + @J2ktIncompatible + public void testAsListTwoSpliteratorIteration() { + List list = Lists.asList("a", "b", new String[] {"c"}); + List collected = new ArrayList<>(); + list.spliterator().forEachRemaining(collected::add); + assertEquals(asList("a", "b", "c"), collected); + } + + @GwtIncompatible // Spliterator + @J2ktIncompatible + public void testCharactersOfSpliteratorCharacteristics() { + List chars = Lists.charactersOf("hello"); + int characteristics = chars.spliterator().characteristics(); + assertTrue("charactersOf should report ORDERED", (characteristics & Spliterator.ORDERED) != 0); + assertTrue("charactersOf should report NONNULL", (characteristics & Spliterator.NONNULL) != 0); + assertTrue("charactersOf should report SIZED", (characteristics & Spliterator.SIZED) != 0); + } + + @GwtIncompatible // Spliterator + @J2ktIncompatible + public void testCharactersOfSpliteratorIteration() { + List chars = Lists.charactersOf("hello"); + List collected = new ArrayList<>(); + chars.spliterator().forEachRemaining(collected::add); + assertEquals(asList('h', 'e', 'l', 'l', 'o'), collected); + } + + @GwtIncompatible // CopyOnWriteArrayList + @J2ktIncompatible + public void testAbstractListWrapperSpliteratorDelegates() { + CopyOnWriteArrayList cow = new CopyOnWriteArrayList<>(asList("a", "b", "c")); + int cowCharacteristics = cow.spliterator().characteristics(); + assertTrue((cowCharacteristics & Spliterator.IMMUTABLE) != 0); + } } diff --git a/guava-tests/test/com/google/common/collect/MapsTest.java b/guava-tests/test/com/google/common/collect/MapsTest.java index 2f857f0dacc6..241581c2e160 100644 --- a/guava-tests/test/com/google/common/collect/MapsTest.java +++ b/guava-tests/test/com/google/common/collect/MapsTest.java @@ -64,6 +64,7 @@ import java.util.Set; import java.util.SortedMap; import java.util.SortedSet; +import java.util.Spliterator; import java.util.TreeMap; import java.util.concurrent.ConcurrentMap; import junit.framework.TestCase; @@ -1624,4 +1625,31 @@ public void testSubMap_unnaturalOrdering() { ImmutableSortedMap.of(2, 0, 4, 0, 6, 0, 8, 0, 10, 0), Maps.subMap(map, Range.all())); } + + @GwtIncompatible // Spliterator + @J2ktIncompatible + public void testKeySetSpliteratorIsDistinct() { + HashMap map = new HashMap<>(); + map.put("a", 1); + map.put("b", 2); + int characteristics = map.keySet().spliterator().characteristics(); + assertTrue( + "keySet spliterator should report DISTINCT", + (characteristics & Spliterator.DISTINCT) != 0); + } + + @GwtIncompatible // Spliterator + @J2ktIncompatible + public void testValuesSpliteratorCharacteristics() { + HashMap map = new HashMap<>(); + map.put("a", 1); + map.put("b", 2); + Spliterator valuesSpliterator = map.values().spliterator(); + assertTrue( + "values spliterator should report SIZED", + (valuesSpliterator.characteristics() & Spliterator.SIZED) != 0); + assertFalse( + "values spliterator should NOT report DISTINCT", + (valuesSpliterator.characteristics() & Spliterator.DISTINCT) != 0); + } } diff --git a/guava/src/com/google/common/collect/CollectSpliterators.java b/guava/src/com/google/common/collect/CollectSpliterators.java index 612f19bcffeb..707623f37d6e 100644 --- a/guava/src/com/google/common/collect/CollectSpliterators.java +++ b/guava/src/com/google/common/collect/CollectSpliterators.java @@ -199,11 +199,15 @@ public long estimateSize() { @Override public int characteristics() { + // IMMUTABLE and CONCURRENT describe the source, not the elements, so filtering + // does not invalidate them. This matches JDK Stream.filter(), which only clears SIZED. return fromSpliterator.characteristics() & (Spliterator.DISTINCT | Spliterator.NONNULL | Spliterator.ORDERED - | Spliterator.SORTED); + | Spliterator.SORTED + | Spliterator.IMMUTABLE + | Spliterator.CONCURRENT); } } return new Splitr(); diff --git a/guava/src/com/google/common/collect/Lists.java b/guava/src/com/google/common/collect/Lists.java index 32e032edd1bf..dc167bea1099 100644 --- a/guava/src/com/google/common/collect/Lists.java +++ b/guava/src/com/google/common/collect/Lists.java @@ -50,6 +50,7 @@ import java.util.NoSuchElementException; import java.util.Objects; import java.util.RandomAccess; +import java.util.Spliterator; import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.Predicate; import org.jspecify.annotations.Nullable; @@ -356,6 +357,14 @@ public E get(int index) { return (index == 0) ? first : rest[index - 1]; } + @Override + @GwtIncompatible + @J2ktIncompatible + public Spliterator spliterator() { + return CollectSpliterators.indexed( + size(), Spliterator.ORDERED | Spliterator.IMMUTABLE | Spliterator.SIZED, this::get); + } + @GwtIncompatible @J2ktIncompatible private static final long serialVersionUID = 0; } @@ -394,6 +403,14 @@ public E get(int index) { } } + @Override + @GwtIncompatible + @J2ktIncompatible + public Spliterator spliterator() { + return CollectSpliterators.indexed( + size(), Spliterator.ORDERED | Spliterator.IMMUTABLE | Spliterator.SIZED, this::get); + } + @GwtIncompatible @J2ktIncompatible private static final long serialVersionUID = 0; } @@ -829,6 +846,14 @@ public Character get(int index) { public int size() { return sequence.length(); } + + @Override + @GwtIncompatible + @J2ktIncompatible + public Spliterator spliterator() { + return CollectSpliterators.indexed( + size(), Spliterator.ORDERED | Spliterator.NONNULL | Spliterator.SIZED, this::get); + } } /** @@ -1201,6 +1226,13 @@ public boolean contains(@Nullable Object o) { public int size() { return backingList.size(); } + + @Override + @GwtIncompatible + @J2ktIncompatible + public Spliterator spliterator() { + return backingList.spliterator(); + } } private static class RandomAccessListWrapper diff --git a/guava/src/com/google/common/collect/Maps.java b/guava/src/com/google/common/collect/Maps.java index d06ca15f2f53..f5f6ceb66e7d 100644 --- a/guava/src/com/google/common/collect/Maps.java +++ b/guava/src/com/google/common/collect/Maps.java @@ -3942,6 +3942,14 @@ public boolean remove(@Nullable Object o) { public void clear() { map().clear(); } + + @Override + @GwtIncompatible + @J2ktIncompatible + public Spliterator spliterator() { + return CollectSpliterators.map( + map().entrySet().spliterator(), Spliterator.DISTINCT, Entry::getKey); + } } static @Nullable K keyOrNull(@Nullable Entry entry) { @@ -4171,6 +4179,13 @@ public boolean contains(@Nullable Object o) { public void clear() { map().clear(); } + + @Override + @GwtIncompatible + @J2ktIncompatible + public Spliterator spliterator() { + return CollectSpliterators.map(map().entrySet().spliterator(), 0, Entry::getValue); + } } abstract static class EntrySet