RFR: 8372946 - TreeMap sub-map entry spliterator is expensive [v4]

Oli Gillespie ogillespie at openjdk.org
Mon Feb 23 14:47:32 UTC 2026


On Mon, 23 Feb 2026 14:40:57 GMT, Oli Gillespie <ogillespie at openjdk.org> wrote:

>> That case still fails, yes, but I'm not totally sure why. I'm looking into it.
>> 
>> 
>> java.util.ConcurrentModificationException
>> 	at java.base/java.util.TreeMap$NavigableSubMap$SubMapIterator.prevEntry(TreeMap.java:2070)
>> 	at java.base/java.util.TreeMap$NavigableSubMap$DescendingSubMapEntryIterator.next(TreeMap.java:2121)
>> 	at java.base/java.util.TreeMap$NavigableSubMap$DescendingSubMapEntryIterator.next(TreeMap.java:2114)
>> 	at java.base/java.util.Iterator.forEachRemaining(Iterator.java:133)
>> 	at java.base/java.util.Spliterators$IteratorSpliterator.forEachRemaining(Spliterators.java:1939)
>> 	at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:570)
>> 	at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:560)
>> 	at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:635)
>> 	at java.base/java.util.stream.AbstractPipeline.evaluateToArrayNode(AbstractPipeline.java:291)
>> 	at java.base/java.util.stream.ReferencePipeline.toArray(ReferencePipeline.java:652)
>> 	at java.base/java.util.stream.ReferencePipeline.toArray(ReferencePipeline.java:658)
>> 	at org.openjdk.tests.java.util.stream.CollectionAndMapModifyStreamTest.testEntrySetSizeRemove(CollectionAndMapModifyStreamTest.java:164)
>> 	at org.openjdk.tests.java.util.stream.CollectionAndMapModifyStreamTest.testMapEntriesSizeRemove(CollectionAndMapModifyStreamTest.java:155)
>
> Oh I understand now. The default Set spliterator is `Spliterator<T> spliterator(Collection<? extends T> c, int characteristics)`. It doesn't create an iterator until forEachRemaining is called, which in the test is *after* the .remove modification, so it doesn't observe a discrepancy. The new implementation uses creates the iterator up-front to pass to `spliteratorUnknownSize`, so in that case the iterator is created before the modification, hence CME.

It ends up something like this:


public static void main(String[] args) {
    List<String> strings = new LinkedList<>();
    strings.add("foo");
    strings.add("bar");

    Spliterator<String> s = Spliterators.spliterator(strings, Spliterator.DISTINCT); // Don't create iterator yet
    strings.remove(strings.iterator().next());
    s.forEachRemaining(System.out::println); // Spliterator creates iterator here, after the .remove call. No CME

    s = Spliterators.spliteratorUnknownSize(strings.iterator(), Spliterator.DISTINCT); // Eagerly create the iterator
    strings.remove(strings.iterator().next()); // Modifying after the iterator was created
    s.forEachRemaining(System.out::println); // ConcurrentModificationException
}


So it's just a side effect of `spliteratorUnknownSize` needing the iterator to be created already. I think the test skip is valid, then - it's true that this case is no longer lazy like it was.

-------------

PR Review Comment: https://git.openjdk.org/jdk/pull/28608#discussion_r2841246021


More information about the core-libs-dev mailing list