diff --git a/guava-tests/test/com/google/common/collect/ListsTest.java b/guava-tests/test/com/google/common/collect/ListsTest.java index abbe3b754ae0..56d2487ff521 100644 --- a/guava-tests/test/com/google/common/collect/ListsTest.java +++ b/guava-tests/test/com/google/common/collect/ListsTest.java @@ -61,6 +61,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; @@ -822,6 +823,62 @@ public void testTransformIteratorSequential() { assertTransformIterator(list); } + @GwtIncompatible // CopyOnWriteArrayList, Spliterator + public void testTransformSpliteratorDoesNotThrowConcurrentModificationException() { + // CopyOnWriteArrayList is documented to never throw ConcurrentModificationException; Lists + // .transform() is documented to return a threadsafe list when the input list and function are + // threadsafe. The spliterator of the transformed list must honor that by delegating to the + // backing list's own (snapshotting) spliterator, rather than falling back to the JDK's default + // index-based RandomAccessSpliterator, which translates concurrent structural changes into a + // ConcurrentModificationException. + CopyOnWriteArrayList fromList = new CopyOnWriteArrayList<>(SOME_LIST); + List transformed = transform(fromList, SOME_FUNCTION); + + Spliterator spliterator = transformed.spliterator(); + List results = new ArrayList<>(); + assertTrue(spliterator.tryAdvance(results::add)); + + // Structurally mutate the backing list after the spliterator was created but before it has + // finished being consumed. + fromList.clear(); + + // Must not throw, and must still report the elements from the original snapshot, exactly as + // fromList.spliterator() itself would. + spliterator.forEachRemaining(results::add); + assertEquals(SOME_STRING_LIST, results); + } + + @GwtIncompatible // CopyOnWriteArrayList, Spliterator + public void testTransformSpliteratorPropagatesBackingListCharacteristics() { + CopyOnWriteArrayList fromList = new CopyOnWriteArrayList<>(SOME_LIST); + List transformed = transform(fromList, SOME_FUNCTION); + + // CopyOnWriteArrayList.spliterator() reports IMMUTABLE (among other bits); a spliterator that + // instead falls back to the default RandomAccessSpliterator would not, since that + // implementation has no way of knowing the backing list is safe to snapshot. + assertTrue(transformed.spliterator().hasCharacteristics(Spliterator.IMMUTABLE)); + } + + @GwtIncompatible // Spliterator + public void testTransformSpliteratorRandomAccess() { + List fromList = new ArrayList<>(SOME_LIST); + List transformed = transform(fromList, SOME_FUNCTION); + + List results = new ArrayList<>(); + transformed.spliterator().forEachRemaining(results::add); + assertEquals(SOME_STRING_LIST, results); + } + + @GwtIncompatible // Spliterator + public void testTransformForEachRandomAccess() { + List fromList = new ArrayList<>(SOME_LIST); + List transformed = transform(fromList, SOME_FUNCTION); + + List results = new ArrayList<>(); + transformed.forEach(results::add); + assertEquals(SOME_STRING_LIST, results); + } + /** * This test depends on the fact that {@code AbstractSequentialList.iterator} transforms the * {@code iterator()} call into a call on {@code listIterator(int)}. This is fine because the diff --git a/guava/src/com/google/common/collect/Lists.java b/guava/src/com/google/common/collect/Lists.java index ae79f5e659c1..ae89af0f80de 100644 --- a/guava/src/com/google/common/collect/Lists.java +++ b/guava/src/com/google/common/collect/Lists.java @@ -51,7 +51,9 @@ 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.Consumer; import java.util.function.Predicate; import org.jspecify.annotations.Nullable; @@ -661,6 +663,18 @@ public boolean isEmpty() { return fromList.isEmpty(); } + @Override + @GwtIncompatible // Spliterator + public Spliterator spliterator() { + return CollectSpliterators.map(fromList.spliterator(), 0, function); + } + + @Override + public void forEach(Consumer action) { + checkNotNull(action); + fromList.forEach((F f) -> action.accept(function.apply(f))); + } + @Override public boolean removeIf(Predicate filter) { checkNotNull(filter);