使用带有 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 上使用retainAllkeySet() 返回的 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 可能是HashMapTreeMap 或其他非并发映射。假设它是HashMap。每个流都由Spliterator 支持。如果 spliterator 没有 IMMUTABLECONCURRENT 特征,那么,如文档所述:

绑定 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 子句时出现奇怪的语法错误

从 Google Firebase 实时数据库读取 google maps 标记数据时出现问题