From 81e27356e568b0f72b8d66b1d1355ea2e1d397c1 Mon Sep 17 00:00:00 2001 From: Wangshu Pang Date: Mon, 3 Aug 2026 19:22:34 -0700 Subject: [PATCH] Make Lists.transform() views delegate spliterator()/forEach() to the backing list TransformingRandomAccessList previously relied on the JDK's default List.spliterator(), which for RandomAccess lists falls back to AbstractList.RandomAccessSpliterator. That implementation is index-based and translates concurrent structural changes into a ConcurrentModificationException, even when the backing list (e.g. CopyOnWriteArrayList) is documented to never throw one. Override spliterator() to delegate to the backing list's own spliterator via CollectSpliterators.map(), and forEach() to delegate directly to the backing list's forEach(), so transformed views inherit the concurrency and other characteristics of their source list. Fixes https://github.com/google/guava/issues/8165 --- .../com/google/common/collect/ListsTest.java | 57 +++++++++++++++++++ .../src/com/google/common/collect/Lists.java | 14 +++++ 2 files changed, 71 insertions(+) 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);