【问题标题】:Java 8 stream API: Exceptions when modifying ListsJava 8 流 API:修改列表时出现异常
【发布时间】:2015-08-15 21:48:56
【问题描述】:

让我们取一个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&lt;&gt;。最后一个带有stream() 的代码给了我一个ConcurentModificationException,而parallelStream() 突然起作用了!

所以,问题。

  1. 内部parallelStream() 厨房(分离器和其他魔法)是否足够聪明,可以对LinkedList 使用这种元素删除?它会一直有效吗?

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

【问题讨论】:

  • 顺便说一下,你可以用Integer.toString(i)代替""+i
  • 如果您因此可以显着提高速度,我不会感到惊讶。
  • 如果您想从列表中删除与谓词匹配的元素,请使用List.removeIf

标签: java java-8 java-stream


【解决方案1】:

您的代码的行为本质上是未定义的(因此您会得到各种答案)。 stream documentation(不干涉部分)声明:

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

并且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)
    

    当从 List 中删除“5”时,从“6”到“9”的所有元素都向左移动了一个位置(即它们的索引减 1)。 Stream管道没有检测到它,所以它跳过了“6”,当它处理最后一个位置(最初包含“9”)时,它遇到了null,导致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)i==9 时会失败。我猜 Stream 管道直接在 ArrayList 的内部数组上工作,所以它没有检测到 List 的大小发生了变化。

    编辑:

    根据 Holger 的评论,我更改了代码以消除 NullPointerException(通过将过滤器更改为 filter(string -&gt; "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)
    

    【讨论】:

    • 值得注意的是,这是特定于实现的,并且可能在不同版本的 JDK 中表现不同。
    • 数组访问并不比Iterator 更“直接”。不同之处在于forEach 操作有一个专门的实现,它使用大小的本地副本,并且不会在每次迭代时检查modCount,而只会在循环后检查一次。 NullPointerException 不是终止了循环吗,ConcurrentModificationException 在循环之后被抛出。拥有大小的本地副本是拆分支持的结果,因为每个块都有自己的索引和大小。
    【解决方案3】:

    如果您想使用流,而不是修改原始集合(请参阅immutability with its inherent thread-safety),您应该只检索一个没有该元素的新列表:

    list.stream().filter(string -> !string.equalsIgnoreCase("5"))
                        .collect(Collectors.toList());
    

    关于您关于parallelStream 的其他问题以及该方法是否始终有效?

    不,绝对不会。您使用的Lists 不是为支持并发访问而构建的,有时它看起来可以工作,有时它会像您看到的那样失败或给您“意外”的结果。如果您知道一个数据结构将被多个线程访问,那么总是相应地编写代码。

    【讨论】:

      【解决方案4】:

      在使用 Java8 Lambda 时,最好不要将堆栈跟踪返回到其表面值。应阅读该错误以了解 NPE 是由 forEach lambda 中的某些代码行引起的。所以,您需要评估每一行,看看是什么原因造成的。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2015-08-29
        • 2011-11-24
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多