lambda - 为什么在不安全操作之后而不是在整个 forEach 循环之后抛出 ConcurrentModificationException ?

标签 lambda java-8 java-stream

通常,当迭代正在进行时进行修改时,会立即引发 ConcurrentModificationException。

但是,使用流 API 的 forEachOrdered 方法,每次修改后都不会抛出异常。相反,在迭代过程中可以成功修改,并最终在整个操作完成后抛出异常。有人可以向我解释这是怎么发生的吗?

下面的代码生成 3 个范围从 -0.5 到 0.5 的随机数。然后过滤掉负值。

        HashMap<Integer, Double> positiveNegative = new HashMap<>();
        for (int i = 0; i < 3; i++) {
            double value =Math.random() - 0.5;
            positiveNegative.put(i, value);
        }

        System.out.println(positiveNegative);

        //filter out the negative value
        positiveNegative.entrySet().stream()
                .filter((entry) -> (entry.getValue() < 0))
                .forEachOrdered(entry -> {
                    System.out.println(entry);
                    positiveNegative.remove(entry.getKey());
                    System.out.println(positiveNegative);
                });

        System.out.println(positiveNegative);
{0=-0.38785299800912576, 1=-0.11085161629013052, 2=-0.3516129030809366}
0=-0.38785299800912576
{1=-0.11085161629013052, 2=-0.3516129030809366}
1=-0.11085161629013052
{2=-0.3516129030809366}
2=-0.3516129030809366
{}

Exception in thread "main" java.util.ConcurrentModificationException
    at java.base/java.util.HashMap$EntrySpliterator.forEachRemaining(HashMap.java:1751)
    at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:484)
    at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:474)
    at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:150)
    at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:173)
    at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
    at java.base/java.util.stream.ReferencePipeline.forEachOrdered(ReferencePipeline.java:502)
    at Solution.main(Solution.java:51)

最佳答案

the documentation说:

Note that the fail-fast behavior of an iterator cannot be guaranteed as it is, generally speaking, impossible to make any hard guarantees in the presence of unsynchronized concurrent modification. Fail-fast iterators throw ConcurrentModificationException on a best-effort basis. Therefore, it would be wrong to write a program that depended on this exception for its correctness: the fail-fast behavior of iterators should be used only to detect bugs.

您的示例中不涉及并发,但要点仍然存在;此功能仅用于检测错误,而不是保证是否或何时引发异常。要检测错误,只要抛出异常就足够了,即使仅在迭代结束时也是如此。

对于 IteratorhasNext()next() 方法,无法控制调用者。调用者不需要迭代到集合的末尾,换句话说,在迭代器被放弃之前,每个方法调用都可能是最后一个方法调用。所以这些方法都不能推迟检查。

相比之下,IteratorSpliteratorforEachRemaining 方法控制迭代循环并知道它们何时退出,因此,可以省略每次迭代中的检查并在最后执行一次检查,假设我们有一个专门的实现,而不是默认方法,它别无选择,只能重复调用单元素迭代方法。

正如您在堆栈跟踪中看到的,forEachOrdered Stream 操作最终在 HashMap$EntrySpliterator.forEachRemaining 处结束,这是一个利用该机会的专门实现。显而易见的原因是性能,因为您不想浪费 CPU 周期来测试每次迭代中的条件,无论如何,当合约允许在最后执行一次时(如果我们从尽最大努力基础契约(Contract)中推断出一项要求)。

关于lambda - 为什么在不安全操作之后而不是在整个 forEach 循环之后抛出 ConcurrentModificationException ?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/57985323/

相关文章:

Java 8 : Where is TriFunction (and kin) in java. util.function?或者有什么选择?

java - Tomcat 7 上的 Websockets 不起作用

java - Java 中对区域设置 (tn-ph) 和 (hmn-cn) 的支持

java - 使用流从 Map 对象列表中仅获取键

lambda - Java 8 流按 3 个字段分组并按总和和计数聚合生成单行输出

c++ - 如何使用 C++ lambda 将成员函数指针转换为普通函数指针以用作回调

python - 列表理解和 for 循环中的 Lambda

java - 使用 Stream API 进行自定义操作

java - 使用流 API 将 List<T> 转换为 Map<T.Prop, List<T>>

exception - 处理java8中CompletableFuture中的运行时异常