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.