使用带有Maps键集的流时出现ConcurrentModificationException
我想从someMap
删除someMap
中不存在哪些键的所有项目。 看看我的代码:
someMap.keySet().stream().filter(v -> !someList.contains(v)).forEach(someMap::remove);
我收到java.util.ConcurrentModificationException
。 为什么? 流不是平行的。 这样做最优雅的方法是什么?
@Eran已经解释了如何更好地解决这个问题。 我将解释为什么发生ConcurrentModificationException
。
发生ConcurrentModificationException
是因为您正在修改流源。 您的Map
可能是HashMap
或TreeMap
或其他非并发地图。 我们假设它是一个HashMap
。 每个流都由Spliterator
支持。 如果spliterator没有IMMUTABLE
和CONCURRENT
特性,那么,正如文档所说:
绑定Spliterator后,如果检测到结构性干扰,应该尽力而为抛出
ConcurrentModificationException
。 执行此操作的Spliterators称为快速失败 。
所以HashMap.keySet().spliterator()
不是IMMUTABLE
(因为这个Set
可以修改)而不是CONCURRENT
(并发更新对于HashMap
是不安全的)。 因此它只检测并发更改并抛出ConcurrentModificationException
作为spliterator文档规定。
还值得引用HashMap
文档:
所有这个类的“集合视图方法”返回的迭代器都是快速失败的 :如果在创建迭代器之后的任何时候对映射进行结构修改,除了通过迭代器自己的remove方法之外,迭代器将抛出
ConcurrentModificationException
。 因此,在并发修改的情况下,迭代器快速而干净地失败,而不是在未来的未确定时间冒任意,非确定性行为的风险。请注意,迭代器的快速失败行为无法得到保证,因为一般来说,在存在不同步的并发修改时,不可能做出任何硬性保证。 失败快速迭代器会尽最大努力抛出
ConcurrentModificationException
。 因此,编写依赖于此exception的程序以确保其正确性是错误的: 迭代器的快速失败行为应该仅用于检测错误 。
虽然它只说关于迭代器,但我相信对于分裂者来说也是如此。
您不需要Stream
API。 在keySet
上使用retainAll
。 对keySet()
返回的Set
任何更改都会反映在原始Map
。
someMap.keySet().retainAll(someList);
您的流调用(逻辑上)与以下内容相同:
for (K k : someMap.keySet()) { if (!someList.contains(k)) { someMap.remove(k); } }
如果你运行它,你会发现它抛出ConcurrentModificationException
,因为它正在迭代它时同时修改地图。 如果您查看文档 ,您会注意到以下内容:
请注意,此exception并不总是表示某个对象已被另一个线程同时修改。 如果单个线程发出违反对象合同的一系列方法调用,则该对象可能会抛出此exception。 例如,如果线程在使用失败快速迭代器迭代集合时直接修改集合,则迭代器将抛出此exception。
这就是你正在做的事情,你正在使用的地图实现显然具有快速失败的迭代器,因此抛出了这个exception。
一种可能的替代方法是直接使用迭代器删除项目:
for (Iterator ks = someMap.keySet().iterator(); ks.hasNext(); ) { K next = ks.next(); if (!someList.contains(k)) { ks.remove(); } }
稍后回答,但您可以在管道中插入一个收集器,以便forEach在一个包含密钥副本的Set上运行:
someMap.keySet() .stream() .filter(v -> !someList.contains(v)) .collect(Collectors.toSet()) .forEach(someMap::remove);