让我们拿一个ArrayList
并用简单的东西填充它:
List<String> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
list.add(""+i);
}
我将尝试使用不同的流API方法删除一个名为5的成员。为此我定义了方法,当使用迭代器的传统迭代时,它将给我一个ConcurentModificationException
。
void removeMember(String clientListener) {
list.remove(clientListener);
}
这段代码给了我这个例外,我明白了:
list.parallelStream()
.filter(string -> string.equalsIgnoreCase("5"))
.forEach(string -> removeMember(string));
但是,仅尝试stream()
,而不是parallelStream()
会产生空指针异常(NPE),这对我来说很奇怪:
list.stream()
.filter(string -> string.equalsIgnoreCase("5"))
.forEach(string -> removeMember(string));
现在将List
类型更改为LinkedList<>
。使用stream()
的最后一个代码为我提供ConcurentModificationException
,parallelStream()
突然有效!
所以,问题。
内部parallelStream()
厨房(Spliterators和其他魔法)是否足够聪明,可以为LinkedList
使用此类元素删除?它会一直有效吗?
为什么ArrayList
的NPE?为什么选择NPE,而不是ConcurentModificationException
我的意思。
答案 0 :(得分:29)
您的代码的行为基本上是未定义的(因此您得到的各种答案)。 stream documentation(非互联网部分)声明:
除非流源是并发的,否则在执行流管道期间修改流的数据源可能会导致异常,错误答案或不一致的行为。
而ArrayList
和LinkedList
不是并发的。
您可以使用并发源,但最好不要修改流的来源,例如使用Collection#removeIf
:
list.removeIf(string -> string.equalsIgnoreCase("5"));
答案 1 :(得分:15)
向管道添加一些调试打印显示NullPointerException的来源:
public <E extends Comparable<E>> void someMethod(E x, E y)
输出:
list.stream().peek(string -> System.out.println("peek1 " + string)).filter(string -> string.equalsIgnoreCase("5")).peek(string -> System.out.println("peek2 " + string)).forEach(string -> removeMember(string));
当从列表中删除“5”时,从“6”到“9”的所有元素都向左移动一个位置(即它们的索引减1)。 Stream管道没有检测到它,所以它跳过了“6”,当它处理了最后一个位置(最初包含“9”)时,它遇到了null,当peek1 0
peek1 1
peek1 2
peek1 3
peek1 4
peek1 5
peek2 5
peek1 7
peek1 8
peek1 9
peek1 null
Exception in thread "main" java.lang.NullPointerException
at HelloWorld.lambda$main$1(HelloWorld.java:22)
at HelloWorld$$Lambda$2/303563356.test(Unknown Source)
at java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:174)
at java.util.stream.ReferencePipeline$11$1.accept(ReferencePipeline.java:373)
at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1374)
at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:512)
at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:502)
at java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151)
at java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174)
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:418)
at HelloWorld.main(HelloWorld.java:22)
时出现NullPointerException
对它进行了评估。
这类似于您在传统string.equalsIgnoreCase("5")
循环中所获得的内容:
for
只有在这里,您才能获得int size = list.size();
for (int i = 0; i < size; i++) {
String string = list.get(i);
if (string.equalsIgnoreCase("5"))
removeMember(string);
}
而不是IndexOutOfBoundsException
,因为NullPointerException
在list.get(i)
时会失败。我想Stream管道直接在i==9
的内部数组上工作,因此它不会检测到List的大小已经改变。
编辑:
根据Holger的评论,我更改了代码以消除ArrayList
(通过将过滤器更改为NullPointerException
)。这确实产生了filter(string -> "5".equalsIgnoreCase(string))
:
ConcurrentModificationException
答案 2 :(得分:6)
如果您想使用流,而不是修改原始集合(请参阅immutability with its inherent thread-safety),您应该只检索没有该元素的新列表:
list.stream().filter(string -> !string.equalsIgnoreCase("5"))
.collect(Collectors.toList());
关于您关于parallelStream
的其他问题以及该方法是否始终有效?
不,绝对不会。您正在使用的Lists
不是为支持并发访问而构建的,有时它似乎可以正常工作,有时它会在您看到或给您“意外”结果时失败。如果您知道多个线程将访问数据结构,则始终相应地进行编码。
答案 3 :(得分:0)
在使用Java8 Lambdas时,最好不要将堆栈跟踪带到其表面值。应该阅读错误以了解NPE是由forEach
lambda.So中的某些代码行引起的,你需要评估每一行,看看是什么导致了这一点。