修改后的底层 Collection 上的 Spliterator

Spliterator on modified underlying Collection

我知道这不应该在生产中发生,但我试图了解有关 Spliterators 的一些复杂细节并碰到以下 "puzzler"(至少对我来说是一个难题):

(片段 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);

此代码按预期打印 456123cough 我已经预料到 ConcurrentModificationException,但我理解 cough),即,它在列表上创建一个 Spliterator,当列表有 6 个元素时,它将被拆分,等等。pp。到目前为止一切顺利。

我不明白的是:

(片段 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);

我预计这段代码会失败,它确实会失败,在 s1.forEachRemaining 行上有一个 ConcurrentModificationException它会 打印 34 也输出到输出中。如果将其更改为 System.err::println,则会看到值 34 分别按照此顺序放入 PrintStream 之前 异常。

现在是疯狂的部分:

(片段 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);

请注意,片段 2 和片段 3 之间的唯一变化是我们访问 s1s2 的顺序。片段 3 仍然失败并显示 ConcurrentModificationException,但打印的值是 12。这是因为异常现在发生在 s2.forEachRemaining!

的行上

如果我理解正确的话,会发生什么:

这是否意味着 Spliterator 也像 Streams 一样 "lazy"?但是,在尝试多次拆分时,即

,这个论点并没有真正成立

(片段 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));

然后应该毫无问题地评估 s1 并在 s2 的处理过程中抛出异常,但在 s1!

的处理过程中它已经抛出异常

感谢任何帮助或指点。

详细信息:我 运行 在 Eclipse 2019-06 (4.12.0) Windows 上 运行 AdoptOpenJDK 11.0.4+11(64 位)的片段,如果重要的话。

使用您的第一个代码段(更正了小错误)

List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4));
Spliterator<Integer> spl1 = list.spliterator();
list.add(5);
list.add(6);
Spliterator<Integer> spl2 = spl1.trySplit();
spl1.forEachRemaining(System.out::print);
spl2.forEachRemaining(System.out::print);

您正在观察 documented behavior 支持更改:

A Spliterator that does not report IMMUTABLE or CONCURRENT is expected to have a documented policy concerning: when the spliterator binds to the element source; and detection of structural interference of the element source detected after binding. A late-binding Spliterator binds to the source of elements at the point of first traversal, first split, or first query for estimated size, rather than at the time the Spliterator is created. A Spliterator that is not late-binding binds to the source of elements at the point of construction or first invocation of any method. Modifications made to the source prior to binding are reflected when the Spliterator is traversed. After binding a Spliterator should, on a best-effort basis, throw ConcurrentModificationException if structural interference is detected. Spliterators that do this are called fail-fast. The bulk traversal method (forEachRemaining()) of a Spliterator may optimize traversal and check for structural interference after all elements have been traversed, rather than checking per-element and failing immediately.

所以在这里,我们看到一个 late-binding 分离器在起作用。在任何遍历之前所做的更改会在遍历期间反映出来。这是 Streamsimilar behavior 的基础,它是根据 Spliterator:

结算的

Unless the stream source is concurrent, modifying a stream's data source during execution of a stream pipeline can cause exceptions, incorrect answers, or nonconformant behavior. For well-behaved stream sources, the source can be modified before the terminal operation commences and those modifications will be reflected in the covered elements. For example, consider the following code:

List<String> l = new ArrayList(Arrays.asList("one", "two"));
Stream<String> sl = l.stream();
l.add("three");
String s = sl.collect(joining(" "));

First a list is created consisting of two strings: "one"; and "two". Then a stream is created from that list. Next the list is modified by adding a third string: "three". Finally the elements of the stream are collected and joined together. Since the list was modified before the terminal collect operation commenced the result will be a string of "one two three".

对于你的其他片段,这句话适用

A late-binding Spliterator binds to the source of elements at the point of first traversal, first split, or first query for estimated size…

因此拆分器在第一次(成功)trySplit 调用时绑定,之后将元素添加到源列表将使它无效,所有拆分器都会从中分离出来。

所以

List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4));
Spliterator<Integer> spl1 = list.spliterator();
Spliterator<Integer> spl2 = spl1.trySplit();
list.add(5);
list.add(6);
spl1.forEachRemaining(System.out::print);
spl2.forEachRemaining(System.out::print);

你得到描述为优化的行为 fail-fast:

The bulk traversal method (forEachRemaining()) of a Spliterator may optimize traversal and check for structural interference after all elements have been traversed, rather than checking per-element and failing immediately.

因此,您会看到调用 forEachRemaining(System.out::print) 的第一个拆分器的元素,然后是 ConcurrentModificationException。更改最后两个语句的顺序只会更改在抛出异常之前打印的元素,与描述完全匹配。

您的最后一个片段仅说明 trySplit 不会自行执行检查。它成功地拆分了一个已经失效的拆分器,导致另一个无效的拆分器。所有生成的拆分器的行为保持不变,第一个拆分器将检测到干扰,但出于性能考虑仅在遍历后才检测到。

我们可以验证所有创建的拆分器的行为是否相同:

List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
Spliterator<Integer> s1 = list.spliterator();
Spliterator<Integer> s2 = s1.trySplit();
list.add(11);
list.add(12);
Spliterator<Integer> s3 = s2.trySplit();
try {            
    s1.forEachRemaining(s -> System.out.println("1 " + s));
} catch(Exception ex) {
    System.out.println("1 "+ex);
}
try {            
    s2.forEachRemaining(s -> System.out.println("2 " + s));
} catch(Exception ex) {
    System.out.println("2 "+ex);
}
try {            
    s3.forEachRemaining(s -> System.out.println("3 " + s));
} catch(Exception ex) {
    System.out.println("3 "+ex);
}
1 6
1 7
1 8
1 9
1 10
1 java.util.ConcurrentModificationException
2 3
2 4
2 5
2 java.util.ConcurrentModificationException
3 1
3 2
3 java.util.ConcurrentModificationException