Java 8 流 API:修改列表时的异常

2022-09-01 20:14:54

让我们拿一个,用简单的东西填充它: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));

但是,尝试只是 ,而不是给出一个空指针异常 (NPE),这对我来说很奇怪:stream()parallelStream()

list.stream()
    .filter(string -> string.equalsIgnoreCase("5"))
    .forEach(string -> removeMember(string));

现在将类型更改为 。最后一个代码给了我一个,突然工作!ListLinkedList<>stream()ConcurentModificationExceptionparallelStream()

所以,问题来了。

  1. 内部厨房(分离器和其他魔法)是否足够聪明,可以使用这样的元素去除?它会一直工作吗?parallelStream()LinkedList

  2. 为什么NPE?为什么是NPE,不是我的意思。ArrayListConcurentModificationException


答案 1

你的代码的行为基本上是未定义的(因此你得到的各种答案)。流文档(非相互引用部分)指出:

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

并且不是并发的。ArrayListLinkedList

您可以使用并发源,但从修改流的源开始会更有意义,例如通过使用 Collection#removeIf

list.removeIf(string -> string.equalsIgnoreCase("5"));

答案 2

将一些调试打印添加到管道将显示 NullPointerException 的源代码:

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));

此输出:

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)

当“5”从清单中删除时,从“6”到“9”的所有要素都向左移动了一个位置(即它们的指数减少了1)。Stream 管道未检测到它,因此它跳过了“6”,当它处理最后一个位置(最初包含“9”)时,它遇到了 null,这会导致何时对其进行评估。NullPointerExceptionstring.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);
}

只有在这里你会得到而不是,因为当.我猜 Stream 管道直接在 的内部数组上工作,因此它不会检测到 List 的大小已更改。IndexOutOfBoundsExceptionNullPointerExceptionlist.get(i)i==9ArrayList

编辑:

根据 Holger 的评论,我更改了代码以消除 (通过将筛选器更改为 )。这确实产生了:NullPointerExceptionfilter(string -> "5".equalsIgnoreCase(string))ConcurrentModificationException

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.util.ConcurrentModificationException
    at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1380)
    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)

推荐