我知道这在生产中永远不会发生,但是我试图了解有关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
,则可以看到3
和4
的值分别按此顺序放在异常之前的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之间的唯一变化是访问s1
和s2
的顺序。代码段3仍然失败,并显示ConcurrentModificationException
,但是打印的值为1
和2
。这是因为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位)上运行代码片段。
答案 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支持这些更改:
不报告
IMMUTABLE
或CONCURRENT
的拆分器应该具有有关以下方面的文档化政策:拆分器绑定到元素源时;结合后检测到的元素源的结构干扰的检测。 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