Skip to content

Commit 6d93913

Browse files
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 #8165
1 parent a63e360 commit 6d93913

File tree

6 files changed

+172
-1
lines changed

6 files changed

+172
-1
lines changed

guava-tests/test/com/google/common/collect/CollectSpliteratorsTest.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.util.Arrays;
2626
import java.util.List;
2727
import java.util.Spliterator;
28+
import java.util.concurrent.CopyOnWriteArrayList;
2829
import java.util.stream.DoubleStream;
2930
import java.util.stream.IntStream;
3031
import java.util.stream.LongStream;
@@ -101,6 +102,32 @@ public void testFlatMapToDouble_nullStream() {
101102
.expect(1.0, 1.0, 2.0, 3.0);
102103
}
103104

105+
@GwtIncompatible // CopyOnWriteArrayList
106+
public void testFilterPreservesImmutableCharacteristic() {
107+
CopyOnWriteArrayList<String> cow = new CopyOnWriteArrayList<>(Arrays.asList("a", "b", "c"));
108+
Spliterator<String> source = cow.spliterator();
109+
assertTrue((source.characteristics() & Spliterator.IMMUTABLE) != 0);
110+
111+
Spliterator<String> filtered =
112+
CollectSpliterators.filter(cow.spliterator(), s -> !s.equals("b"));
113+
assertTrue(
114+
"filter() should preserve IMMUTABLE from source",
115+
(filtered.characteristics() & Spliterator.IMMUTABLE) != 0);
116+
}
117+
118+
@GwtIncompatible
119+
public void testFilterDropsSized() {
120+
Spliterator<String> source =
121+
Arrays.spliterator(new String[] {"a", "b", "c"});
122+
assertTrue((source.characteristics() & Spliterator.SIZED) != 0);
123+
124+
Spliterator<String> filtered =
125+
CollectSpliterators.filter(source, s -> !s.equals("b"));
126+
assertFalse(
127+
"filter() should not preserve SIZED",
128+
(filtered.characteristics() & Spliterator.SIZED) != 0);
129+
}
130+
104131
public void testMultisetsSpliterator() {
105132
Multiset<String> multiset = TreeMultiset.create();
106133
multiset.add("a", 3);

guava-tests/test/com/google/common/collect/ListsTest.java

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
import java.util.ListIterator;
5858
import java.util.NoSuchElementException;
5959
import java.util.RandomAccess;
60+
import java.util.Spliterator;
6061
import java.util.concurrent.CopyOnWriteArrayList;
6162
import junit.framework.Test;
6263
import junit.framework.TestCase;
@@ -998,4 +999,68 @@ public void testPartitionSize_1() {
998999
public void testPartitionSize_2() {
9991000
assertEquals(2, partition(nCopies(0x40000001, 1), 0x40000000).size());
10001001
}
1002+
1003+
@GwtIncompatible // Spliterator
1004+
@J2ktIncompatible
1005+
public void testAsListSpliteratorIsImmutable() {
1006+
List<String> list = Lists.asList("a", new String[] {"b", "c"});
1007+
int characteristics = list.spliterator().characteristics();
1008+
assertTrue("asList should report IMMUTABLE", (characteristics & Spliterator.IMMUTABLE) != 0);
1009+
assertTrue("asList should report ORDERED", (characteristics & Spliterator.ORDERED) != 0);
1010+
assertTrue("asList should report SIZED", (characteristics & Spliterator.SIZED) != 0);
1011+
}
1012+
1013+
@GwtIncompatible // Spliterator
1014+
@J2ktIncompatible
1015+
public void testAsListSpliteratorIteration() {
1016+
List<String> list = Lists.asList("a", new String[] {"b", "c"});
1017+
List<String> collected = new ArrayList<>();
1018+
list.spliterator().forEachRemaining(collected::add);
1019+
assertEquals(asList("a", "b", "c"), collected);
1020+
}
1021+
1022+
@GwtIncompatible // Spliterator
1023+
@J2ktIncompatible
1024+
public void testAsListTwoSpliteratorIsImmutable() {
1025+
List<String> list = Lists.asList("a", "b", new String[] {"c"});
1026+
int characteristics = list.spliterator().characteristics();
1027+
assertTrue(
1028+
"asList(2+) should report IMMUTABLE", (characteristics & Spliterator.IMMUTABLE) != 0);
1029+
}
1030+
1031+
@GwtIncompatible // Spliterator
1032+
@J2ktIncompatible
1033+
public void testAsListTwoSpliteratorIteration() {
1034+
List<String> list = Lists.asList("a", "b", new String[] {"c"});
1035+
List<String> collected = new ArrayList<>();
1036+
list.spliterator().forEachRemaining(collected::add);
1037+
assertEquals(asList("a", "b", "c"), collected);
1038+
}
1039+
1040+
@GwtIncompatible // Spliterator
1041+
@J2ktIncompatible
1042+
public void testCharactersOfSpliteratorCharacteristics() {
1043+
List<Character> chars = Lists.charactersOf("hello");
1044+
int characteristics = chars.spliterator().characteristics();
1045+
assertTrue("charactersOf should report ORDERED", (characteristics & Spliterator.ORDERED) != 0);
1046+
assertTrue("charactersOf should report NONNULL", (characteristics & Spliterator.NONNULL) != 0);
1047+
assertTrue("charactersOf should report SIZED", (characteristics & Spliterator.SIZED) != 0);
1048+
}
1049+
1050+
@GwtIncompatible // Spliterator
1051+
@J2ktIncompatible
1052+
public void testCharactersOfSpliteratorIteration() {
1053+
List<Character> chars = Lists.charactersOf("hello");
1054+
List<Character> collected = new ArrayList<>();
1055+
chars.spliterator().forEachRemaining(collected::add);
1056+
assertEquals(asList('h', 'e', 'l', 'l', 'o'), collected);
1057+
}
1058+
1059+
@GwtIncompatible // CopyOnWriteArrayList
1060+
@J2ktIncompatible
1061+
public void testAbstractListWrapperSpliteratorDelegates() {
1062+
CopyOnWriteArrayList<String> cow = new CopyOnWriteArrayList<>(asList("a", "b", "c"));
1063+
int cowCharacteristics = cow.spliterator().characteristics();
1064+
assertTrue((cowCharacteristics & Spliterator.IMMUTABLE) != 0);
1065+
}
10011066
}

guava-tests/test/com/google/common/collect/MapsTest.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
import java.util.Set;
6565
import java.util.SortedMap;
6666
import java.util.SortedSet;
67+
import java.util.Spliterator;
6768
import java.util.TreeMap;
6869
import java.util.concurrent.ConcurrentMap;
6970
import junit.framework.TestCase;
@@ -1624,4 +1625,31 @@ public void testSubMap_unnaturalOrdering() {
16241625
ImmutableSortedMap.of(2, 0, 4, 0, 6, 0, 8, 0, 10, 0),
16251626
Maps.subMap(map, Range.<Integer>all()));
16261627
}
1628+
1629+
@GwtIncompatible // Spliterator
1630+
@J2ktIncompatible
1631+
public void testKeySetSpliteratorIsDistinct() {
1632+
HashMap<String, Integer> map = new HashMap<>();
1633+
map.put("a", 1);
1634+
map.put("b", 2);
1635+
int characteristics = map.keySet().spliterator().characteristics();
1636+
assertTrue(
1637+
"keySet spliterator should report DISTINCT",
1638+
(characteristics & Spliterator.DISTINCT) != 0);
1639+
}
1640+
1641+
@GwtIncompatible // Spliterator
1642+
@J2ktIncompatible
1643+
public void testValuesSpliteratorCharacteristics() {
1644+
HashMap<String, Integer> map = new HashMap<>();
1645+
map.put("a", 1);
1646+
map.put("b", 2);
1647+
Spliterator<Integer> valuesSpliterator = map.values().spliterator();
1648+
assertTrue(
1649+
"values spliterator should report SIZED",
1650+
(valuesSpliterator.characteristics() & Spliterator.SIZED) != 0);
1651+
assertFalse(
1652+
"values spliterator should NOT report DISTINCT",
1653+
(valuesSpliterator.characteristics() & Spliterator.DISTINCT) != 0);
1654+
}
16271655
}

guava/src/com/google/common/collect/CollectSpliterators.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,11 +199,15 @@ public long estimateSize() {
199199

200200
@Override
201201
public int characteristics() {
202+
// IMMUTABLE and CONCURRENT describe the source, not the elements, so filtering
203+
// does not invalidate them. This matches JDK Stream.filter(), which only clears SIZED.
202204
return fromSpliterator.characteristics()
203205
& (Spliterator.DISTINCT
204206
| Spliterator.NONNULL
205207
| Spliterator.ORDERED
206-
| Spliterator.SORTED);
208+
| Spliterator.SORTED
209+
| Spliterator.IMMUTABLE
210+
| Spliterator.CONCURRENT);
207211
}
208212
}
209213
return new Splitr();

guava/src/com/google/common/collect/Lists.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
import java.util.NoSuchElementException;
5151
import java.util.Objects;
5252
import java.util.RandomAccess;
53+
import java.util.Spliterator;
5354
import java.util.concurrent.CopyOnWriteArrayList;
5455
import java.util.function.Predicate;
5556
import org.jspecify.annotations.Nullable;
@@ -356,6 +357,14 @@ public E get(int index) {
356357
return (index == 0) ? first : rest[index - 1];
357358
}
358359

360+
@Override
361+
@GwtIncompatible
362+
@J2ktIncompatible
363+
public Spliterator<E> spliterator() {
364+
return CollectSpliterators.indexed(
365+
size(), Spliterator.ORDERED | Spliterator.IMMUTABLE | Spliterator.SIZED, this::get);
366+
}
367+
359368
@GwtIncompatible @J2ktIncompatible private static final long serialVersionUID = 0;
360369
}
361370

@@ -394,6 +403,14 @@ public E get(int index) {
394403
}
395404
}
396405

406+
@Override
407+
@GwtIncompatible
408+
@J2ktIncompatible
409+
public Spliterator<E> spliterator() {
410+
return CollectSpliterators.indexed(
411+
size(), Spliterator.ORDERED | Spliterator.IMMUTABLE | Spliterator.SIZED, this::get);
412+
}
413+
397414
@GwtIncompatible @J2ktIncompatible private static final long serialVersionUID = 0;
398415
}
399416

@@ -829,6 +846,14 @@ public Character get(int index) {
829846
public int size() {
830847
return sequence.length();
831848
}
849+
850+
@Override
851+
@GwtIncompatible
852+
@J2ktIncompatible
853+
public Spliterator<Character> spliterator() {
854+
return CollectSpliterators.indexed(
855+
size(), Spliterator.ORDERED | Spliterator.NONNULL | Spliterator.SIZED, this::get);
856+
}
832857
}
833858

834859
/**
@@ -1201,6 +1226,13 @@ public boolean contains(@Nullable Object o) {
12011226
public int size() {
12021227
return backingList.size();
12031228
}
1229+
1230+
@Override
1231+
@GwtIncompatible
1232+
@J2ktIncompatible
1233+
public Spliterator<E> spliterator() {
1234+
return backingList.spliterator();
1235+
}
12041236
}
12051237

12061238
private static class RandomAccessListWrapper<E extends @Nullable Object>

guava/src/com/google/common/collect/Maps.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3942,6 +3942,14 @@ public boolean remove(@Nullable Object o) {
39423942
public void clear() {
39433943
map().clear();
39443944
}
3945+
3946+
@Override
3947+
@GwtIncompatible
3948+
@J2ktIncompatible
3949+
public Spliterator<K> spliterator() {
3950+
return CollectSpliterators.map(
3951+
map().entrySet().spliterator(), Spliterator.DISTINCT, Entry::getKey);
3952+
}
39453953
}
39463954

39473955
static <K extends @Nullable Object> @Nullable K keyOrNull(@Nullable Entry<K, ?> entry) {
@@ -4171,6 +4179,13 @@ public boolean contains(@Nullable Object o) {
41714179
public void clear() {
41724180
map().clear();
41734181
}
4182+
4183+
@Override
4184+
@GwtIncompatible
4185+
@J2ktIncompatible
4186+
public Spliterator<V> spliterator() {
4187+
return CollectSpliterators.map(map().entrySet().spliterator(), 0, Entry::getValue);
4188+
}
41744189
}
41754190

41764191
abstract static class EntrySet<K extends @Nullable Object, V extends @Nullable Object>

0 commit comments

Comments
 (0)