Java 8流API:修改列表时的例外情况

时间:2015-06-02 07:51:59

标签: java java-8 java-stream

让我们拿一个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()的最后一个代码为我提供ConcurentModificationExceptionparallelStream()突然有效!

所以,问题。

  1. 内部parallelStream()厨房(Spliterators和其他魔法)是否足够聪明,可以为LinkedList使用此类元素删除?它会一直有效吗?

  2. 为什么ArrayList的NPE?为什么选择NPE,而不是ConcurentModificationException我的意思。

4 个答案:

答案 0 :(得分:29)

您的代码的行为基本上是未定义的(因此您得到的各种答案)。 stream documentation(非互联网部分)声明:

  

除非流源是并发的,否则在执行流管道期间修改流的数据源可能会导致异常,错误答案或不一致的行为。

ArrayListLinkedList不是并发的。

您可以使用并发源,但最好不要修改流的来源,例如使用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,因为NullPointerExceptionlist.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中的某些代码行引起的,你需要评估每一行,看看是什么导致了这一点。