使用带有Maps键集的流时出现ConcurrentModificationException

jas*_*mar 19 java foreach lambda hashmap java-stream

我想删除someMap不存在密钥的所有项目someList.看看我的代码:

someMap.keySet().stream().filter(v -> !someList.contains(v)).forEach(someMap::remove);
Run Code Online (Sandbox Code Playgroud)

我收到了java.util.ConcurrentModificationException.为什么?流不是平行的.这样做最优雅的方法是什么?

Tag*_*eev 28

@Eran已经解释了如何更好地解决这个问题.我会解释为什么ConcurrentModificationException会发生.

ConcurrentModificationException发生这种情况是因为您正在修改流源.您Map可能是HashMap或者TreeMap其他非并发地图.我们假设它是一个HashMap.每个流都有支持Spliterator.如果spliterator没有IMMUTABLECONCURRENT特征,那么,正如文档所说:

绑定Spliterator后,ConcurrentModificationException如果检测到结构性干扰,应尽力投入.执行此操作的Spliterators称为fail-fast.

所以HashMap.keySet().spliterator()不是IMMUTABLE(因为这Set可以修改)而不是CONCURRENT(并发更新是不安全的HashMap).所以它只是检测并发更改并抛出一个ConcurrentModificationExceptionspliterator文档规定.

还值得引用HashMap文档:

所有这个类的"集合视图方法"返回的迭代器都是快速失败的:如果在创建迭代器之后的任何时候对映射进行结构修改,除了通过迭代器自己的remove方法之外,迭代器将抛出一个ConcurrentModificationException.因此,在并发修改的情况下,迭代器快速而干净地失败,而不是在未来的未确定时间冒任意,非确定性行为的风险.

请注意,迭代器的故障快速行为无法得到保证,因为一般来说,在存在不同步的并发修改时,不可能做出任何硬性保证.快速失败的迭代器会ConcurrentModificationException尽力而为.因此,编写依赖于此异常的程序以确保其正确性是错误的:迭代器的快速失败行为应该仅用于检测错误.

虽然它只说关于迭代器,但我相信它对于分裂者来说是一样的.

  • @MariuszJaskółka,Eran的回答也在这里,其他人也可能会看到它.这是正确的,我赞成它.我可以添加对他的解决方案的引用. (2认同)

Era*_*ran 13

您不需要StreamAPI.使用retainAllkeySet.Set返回的任何更改都会keySet()反映在原始文件中Map.

someMap.keySet().retainAll(someList);
Run Code Online (Sandbox Code Playgroud)


the*_*oop 9

您的流调用(逻辑上)与以下内容相同:

for (K k : someMap.keySet()) {
    if (!someList.contains(k)) {
        someMap.remove(k);
    }
}
Run Code Online (Sandbox Code Playgroud)

如果你运行它,你会发现它会抛出ConcurrentModificationException,因为它在你迭代它的同时修改了地图.如果您查看文档,您会注意到以下内容:

请注意,此异常并不总是表示某个对象已被另一个线程同时修改.如果单个线程发出违反对象合同的一系列方法调用,则该对象可能会抛出此异常.例如,如果线程在使用失败快速迭代器迭代集合时直接修改集合,则迭代器将抛出此异常.

这就是你正在做的事情,你正在使用的地图实现显然具有快速失败的迭代器,因此抛出了这个异常.

一种可能的替代方法是直接使用迭代器删除项目:

for (Iterator<K> ks = someMap.keySet().iterator(); ks.hasNext(); ) {
    K next = ks.next();
    if (!someList.contains(k)) {
        ks.remove();
    }
}
Run Code Online (Sandbox Code Playgroud)


Ron*_*eod 7

稍后回答,但您可以在管道中插入一个收集器,以便 forEach 对包含键副本的 Set 进行操作:

someMap.keySet()
    .stream()
    .filter(v -> !someList.contains(v))
    .collect(Collectors.toSet())
    .forEach(someMap::remove);
Run Code Online (Sandbox Code Playgroud)