使用带有 Maps 键集的流时出现 ConcurrentModificationException
Posted
技术标签:
【中文标题】使用带有 Maps 键集的流时出现 ConcurrentModificationException【英文标题】:ConcurrentModificationException when using stream with Maps key set 【发布时间】:2015-12-11 09:34:53 【问题描述】:我想从someMap
中删除所有在someList
中不存在的项。看看我的代码:
someMap.keySet().stream().filter(v -> !someList.contains(v)).forEach(someMap::remove);
我收到java.util.ConcurrentModificationException
。为什么?流不是并行的。最优雅的方法是什么?
【问题讨论】:
【参考方案1】:您不需要Stream
API。在keySet
上使用retainAll
。 keySet()
返回的 Set
上的任何更改都会反映在原始 Map
中。
someMap.keySet().retainAll(someList);
【讨论】:
好的,这是我第二个问题的好答案。但是我还是不知道为什么会出现java.util.ConcurrentModificationException
。【参考方案2】:
您的流调用(在逻辑上)与以下内容相同:
for (K k : someMap.keySet())
if (!someList.contains(k))
someMap.remove(k);
如果你运行它,你会发现它抛出ConcurrentModificationException
,因为它在你迭代它的同时修改了地图。如果您查看docs,您会注意到以下内容:
请注意,此异常并不总是表示对象已被不同的线程同时修改。如果单个线程发出一系列违反对象约定的方法调用,则该对象可能会抛出此异常。例如,如果线程在使用快速失败迭代器迭代集合时直接修改集合,则迭代器将抛出此异常。
这就是你正在做的事情,你正在使用的地图实现显然有快速失败的迭代器,因此抛出了这个异常。
一种可能的替代方法是直接使用迭代器删除项目:
for (Iterator<K> ks = someMap.keySet().iterator(); ks.hasNext(); )
K next = ks.next();
if (!someList.contains(k))
ks.remove();
【讨论】:
【参考方案3】:@Eran 已经explained 如何更好地解决这个问题。我会解释为什么会出现ConcurrentModificationException
。
ConcurrentModificationException
出现是因为您正在修改流源。您的Map
可能是HashMap
或TreeMap
或其他非并发映射。假设它是HashMap
。每个流都由Spliterator
支持。如果 spliterator 没有 IMMUTABLE
和 CONCURRENT
特征,那么,如文档所述:
绑定 Spliterator 后,如果检测到结构干扰,应尽最大努力抛出
ConcurrentModificationException
。执行此操作的拆分器称为 fail-fast。
所以HashMap.keySet().spliterator()
不是IMMUTABLE
(因为这个Set
可以修改)而不是CONCURRENT
(并发更新对于HashMap
是不安全的)。所以它只是检测并发更改并按照拆分器文档的规定抛出ConcurrentModificationException
。
还值得引用HashMap
文档:
这个类的所有“集合视图方法”返回的迭代器都是fail-fast:如果地图在迭代器创建后的任何时间被结构修改,除了通过迭代器的自己的 remove 方法,迭代器会抛出一个
ConcurrentModificationException
。因此,面对并发修改,迭代器会快速而干净地失败,而不是在未来不确定的时间冒任意的、非确定性的行为。请注意,无法保证迭代器的快速失败行为,因为一般来说,在存在不同步的并发修改的情况下无法做出任何硬保证。快速失败的迭代器会尽最大努力抛出
ConcurrentModificationException
。因此,编写一个依赖此异常来确保其正确性的程序是错误的:迭代器的快速失败行为应该只用于检测错误。
虽然它只说迭代器,但我相信拆分器也是如此。
【讨论】:
我认为这是最好的答案,但您可以编辑它以添加@Eran 提到的解决方案。对于将来遇到同样问题的人来说,这将是 100% 的满足。 @MariuszJaskółka,Eran 的答案也在这里,其他人也可能会看到它。这是正确的,我赞成。我可以添加对他的解决方案的引用。【参考方案4】:稍后回答,但您可以将收集器插入到您的管道中,以便 forEach 在包含密钥副本的 Set 上运行:
someMap.keySet()
.stream()
.filter(v -> !someList.contains(v))
.collect(Collectors.toSet())
.forEach(someMap::remove);
【讨论】:
以上是关于使用带有 Maps 键集的流时出现 ConcurrentModificationException的主要内容,如果未能解决你的问题,请参考以下文章
从另一个获取 NSDictionary 的 NSArray,其中包含原始键集的子集
尝试使用 Google Maps API 旋转地图时出现问题
远程在 Firefox 中使用 Google Maps V3 时出现“未定义 google”
尝试安装 React-native-maps 时出现此错误,这是啥意思?
在 Toad for Sql Server 2016 中使用带有 Union All 的 Order By 子句时出现奇怪的语法错误