修改后的底层 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);
此代码按预期打印 456123
(cough 我已经预料到 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
,则会看到值 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
!
的行上
如果我理解正确的话,会发生什么:
- Spliterator 已初始化
- 拆分完成
- 迭代发生
- 在迭代期间,观察到基础集合发生了修改,在最后一次拆分完成后
这是否意味着 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 分离器在起作用。在任何遍历之前所做的更改会在遍历期间反映出来。这是 Stream
的 similar 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
我知道这不应该在生产中发生,但我试图了解有关 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);
此代码按预期打印 456123
(cough 我已经预料到 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
,则会看到值 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
!
如果我理解正确的话,会发生什么:
- Spliterator 已初始化
- 拆分完成
- 迭代发生
- 在迭代期间,观察到基础集合发生了修改,在最后一次拆分完成后
这是否意味着 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
orCONCURRENT
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, throwConcurrentModificationException
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 分离器在起作用。在任何遍历之前所做的更改会在遍历期间反映出来。这是 Stream
的 similar 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