我想从 someMap
中移除那些键不在 someList
中的所有项。
看一下我的代码:
someMap.keySet()
.stream()
.filter(v -> !someList.contains(v))
.forEach(someMap::remove);
我收到了 java.util.ConcurrentModificationException
异常。
既然流不是并行的,为什么会出现这个异常?
最优雅的解决方法是什么?
我想从 someMap
中移除那些键不在 someList
中的所有项。
看一下我的代码:
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后,如果检测到结构干扰,则Spliterator应尽最大努力抛出
ConcurrentModificationException
。这样做的Spliterator被称为fail-fast。
HashMap.keySet().spliterator()
不是IMMUTABLE
(因为这个Set
可以被修改),也不是CONCURRENT
(对于HashMap
来说并发更新是不安全的)。因此,它只是检测到并发更改并按照spliterator文档的规定抛出一个ConcurrentModificationException
。HashMap
文档:请注意,迭代器的快速失败行为不能得到保证,因为一般来说,在未同步的并发修改存在的情况下,不可能做出任何硬性保证。快速失败迭代器尽最大努力抛出ConcurrentModificationException异常。因此,编写依赖于此异常的程序来保证其正确性是错误的:迭代器的快速失败行为只应用于检测bug。
虽然它只提到了迭代器,但我认为Spliterator也是一样的。
你不需要使用Stream
API。可以在keySet
上使用retainAll
方法。对keySet()
返回的Set
进行的任何更改都会反映在原始的Map
中。
someMap.keySet().retainAll(someList);
java.util.ConcurrentModificationException
。 - Mariusz Jaskółka你的流调用(逻辑上)与以下代码执行的操作相同:
for (K k : someMap.keySet()) {
if (!someList.contains(k)) {
someMap.remove(k);
}
}
如果运行此代码,你将会发现它会抛出 ConcurrentModificationException
异常,因为它在你遍历 map 的同时修改了它。如果你查看 文档,你会注意到以下内容:
请注意,此异常并不总是表示对象已被不同的线程并发修改。如果单个线程发出一系列违反对象合同的方法调用序列,则该对象可能会抛出此异常。例如,如果一个线程直接修改集合,而另一个使用 fail-fast 迭代器遍历该集合,则迭代器将抛出此异常。
这就是你正在做的事情,你正在使用显然具有 fail-fast 迭代器的 map 实现,因此会抛出此异常。
一种可能的替代方法是直接使用迭代器删除项目:
for (Iterator<K> ks = someMap.keySet().iterator(); ks.hasNext(); ) {
K next = ks.next();
if (!someList.contains(k)) {
ks.remove();
}
}
someMap.keySet()
.stream()
.filter(v -> !someList.contains(v))
.collect(Collectors.toSet())
.forEach(someMap::remove);