I know that this should never occur in production, but I'm trying to understand some intricate details about Spliterators and bumped into the following "puzzler" (at least a puzzler for me):
(Snippet 1)
List<Integer> list = new ArrayList<>() {{ add(1); add(2); add(3); add(4); }};
Spliterator<Integer> spl1 = list.spliterator();
list.add(5);
list.add(6);
Spliterator<Integer> spl2 = s1.trySplit();
s1.forEachRemaining(System.out::print);
s2.forEachRemaining(System.out::print);
This code prints 456123
, as expected (cough I already expected a ConcurrentModificationException
, but I understand the behaviour cough), i.e., it creates a Spliterator on the list, which will be split when the list has 6 elements, etc. pp. So far so good.
What I don't understand is the following:
(Snippet 2)
List<Integer> list = new ArrayList<>() {{ add(1); add(2); add(3); add(4); }};
Spliterator<Integer> spl1 = list.spliterator();
Spliterator<Integer> spl2 = s1.trySplit();
list.add(5);
list.add(6);
s1.forEachRemaining(System.out::print);
s2.forEachRemaining(System.out::print);
I expect this code to fail, and it does, with a ConcurrentModificationException
on the line for s1.forEachRemaining
, it will however print 34
onto the output too. If it's changed to System.err::println
, one sees that the values 3
and 4
are in this respective order put onto the PrintStream
before the Exception.
Now the crazy part:
(Snippet 3)
List<Integer> list = new ArrayList<>() {{ add(1); add(2); add(3); add(4); }};
Spliterator<Integer> spl1 = list.spliterator();
Spliterator<Integer> spl2 = s1.trySplit();
list.add(5);
list.add(6);
s2.forEachRemaining(System.out::print);
s1.forEachRemaining(System.out::print);
Note that the only change between Snippet 2 and 3 is the order in which we access s1
and s2
. Snippet 3 still fails with a ConcurrentModificationException
, but the values printed are 1
and 2
. This is, because the Exception happens now on the line for s2.forEachRemaining
!
If I understand correctly, what happens is:
- the Spliterator is initialized
- the split is done
- the iteration happens
- during the iteration it is observed that there has been a modification on the underlying collection, after the last split has been done
Does this mean that Spliterators are also "lazy", just like Streams? However, this argument doesn't really hold up, when experimenting with multiple splits, i.e.,
(Snippet 4)
List<Integer> list = new ArrayList<>() {{ add(1); add(2); add(3); add(4); add(5); add(6); add(7); add(8); add(9); add(10); }};
Spliterator<Integer> s1 = list.spliterator();
Spliterator<Integer> s2 = s1.trySplit();
list.add(11);
list.add(12);
Spliterator<Integer> s3 = s2.trySplit();
s1.forEachRemaining(s -> System.err.println("1 " + s));
s2.forEachRemaining(s -> System.err.println("2 " + s));
s3.forEachRemaining(s -> System.err.println("3 " + s));
Should then evaluate s1
without problems and throw an exception during the processing of s2
, but it already throws and exception during the processing of s1
!
Any help or pointers are appreciated.
Details: I run the snippets on AdoptOpenJDK 11.0.4+11 (64-bit) on Windows in Eclipse 2019-06 (4.12.0), if it matters.
With your first snippet (minor errors corrected)
you are observing a documented behavior of supporting the changes:
So here, we see a late-binding spliterator in action. Changes made prior any traversal are reflected during the traversal. This is the basis for the similar behavior of a
Stream
which is settled on aSpliterator
:For your other snippets, the sentence applies
So the spliterator is bound at the first (successful)
trySplit
call and adding elements afterwards to the source list will invalidate it and all spliterators split off from it.So with
you get the behavior described as optimized fail-fast:
So you see the elements of the first spliterator you call
forEachRemaining(System.out::print)
on, followed by aConcurrentModificationException
. Changing the order of the last two statements just changes which elements are printed before the exception is thrown, perfectly matching the description.Your last snippet only demonstrates that
trySplit
doesn’t perform a check on its own. It successfully splits an already invalidated spliterator, resulting in another invalid spliterator. The behavior of all resulting spliterators stays the same, the first spliterator will detect the interference, but due to performance considerations only after the traversal.We can verify that all created spliterators behave the same: