修改后的基础集合上的分隔符

时间:2019-10-28 17:11:26

标签: java spliterator

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

(摘要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);

此代码按预期方式打印456123咳嗽,我已经期望有ConcurrentModificationException,但我了解行为咳嗽),即它会在列表上创建一个拆分器,当列表中有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);

我希望此代码失败,并且确实如此,并且在ConcurrentModificationException的行上有s1.forEachRemaining,但它会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的行上现在发生了异常!

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

  • 分离器已初始化
  • 拆分完成
  • 迭代发生
    • 期间,观察到底层集合进行了修改,在 最后一次分割完成之后

这是否意味着分隔符也像流一样“惰性”?但是,在尝试多个拆分(即

)时,这种说法并没有真正成立

(摘要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时它已经抛出了异常!

感谢任何帮助或指示。

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

1 个答案:

答案 0 :(得分:2)

第一个代码段(已纠正一些小错误)

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支持这些更改:

  

不报告IMMUTABLECONCURRENT的拆分器应该具有有关以下方面的文档化政策:拆分器绑定到元素源时;结合后检测到的元素源的结构干扰的检测。 late-binding 分隔符在第一次遍历,第一次拆分或首次查询估计大小时,而不是在创建分隔符时绑定到元素的源。不是 late-binding 的Spliterator在构造或第一次调用任何方法时绑定到元素的源。遍历Spliterator时,会反映出绑定之前对源所做的修改。绑定后,如果检测到结构干扰,分离器应尽最大努力抛出ConcurrentModificationException。执行此操作的拆分器称为 fail-fast 。拆分器的批量遍历方法(forEachRemaining())可以优化遍历并在遍历所有元素之后检查结构干涉,而不是检查每个元素并立即失效。

因此,在这里,我们看到了一个 late-binding 分隔符在起作用。在遍历之前进行的更改会在遍历期间反映出来。这是Stream的{​​{3}}的基础,它基于Spliterator

  

除非流源是并发的,否则在流管道执行期间修改流的数据源可能会导致异常,错误答案或不符合要求的行为。对于行为良好的流源,可以在终端操作开始之前修改源,并且这些修改将反映在所涵盖的元素中。例如,考虑以下代码:

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

首先创建一个列表,该列表由两个字符串组成:“一个”;和“两个”。然后从该列表创建一个流。接下来,通过添加第三个字符串“三”来修改列表。最后,将流中的元素收集起来并结合在一起。由于列表是在终端collect操作开始之前修改的,因此结果将是字符串“一二三”。

对于其他片段,该句子适用

  

late-binding 分隔符在第一次遍历,第一次拆分或第一次查询估计大小时绑定到元素的源……

因此,分隔符在第一次(成功的)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);

您将获得描述为优化的快速失败的行为:

  

拆分器的批量遍历方法(forEachRemaining())可以优化遍历并在遍历所有元素之后检查结构干涉,而不是检查每个元素并立即失效。

因此,您看到第一个分隔符的元素,在其上调用了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