2015-09-15 48 views
8

我想要从someMap中删除所有项,哪些密钥不在someList中。看看我的代码:ConcurrentModificationException使用流映射密钥集时

someMap.keySet().stream().filter(v -> !someList.contains(v)).forEach(someMap::remove); 

我收到java.util.ConcurrentModificationException。为什么?流不平行。什么是最优雅的方式来做到这一点?

回答

15

@Eran已经explained如何解决这个问题更好。我将解释为什么ConcurrentModificationException发生。由于您正在修改流源,因此发生ConcurrentModificationException。您的Map很可能是HashMapTreeMap或其他非并发地图。我们假设这是一个HashMap。每个流都支持Spliterator。如果spliterator没有IMMUTABLECONCURRENT特征,那么,作为文档说:

结合Spliterator后应,尽最大努力的基础上,如果检测到结构的干扰扔ConcurrentModificationException。这样做的Spliterators被称为失效快速

所以HashMap.keySet().spliterator()IMMUTABLE(因为这Set可以修改),而不是CONCURRENT(并发更新是不安全的HashMap)。因此,它只是检测到并发更改,并在spliterator文档规定的情况下抛出ConcurrentModificationException

另外值得援引HashMap文件:所有的此类的“collection视图方法”返回

的迭代器都是快速失败的:如果地图随时迭代后结构修饰除了通过迭代器自己的remove方法以外的任何方式创建,迭代器都会抛出ConcurrentModificationException。因此,面对并发修改,迭代器快速而干净地失败,而不是在将来未定的时间冒着任意的,非确定性的行为冒险。

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

虽然它只说了迭代器,但我相信这对于分割器来说也是一样的。

+0

我认为这是最好的答案,但您可以编辑它以添加@Eran提到的解决方案。对于将来有同样问题的人来说,这将是100%满意的。 – jaskmar

+1

@MariuszJaskółka,伊兰的答案也在这里,其他人也可能会看到它。这是正确的,我赞成它。我可以添加对他的解决方案的参考。 –

8

你不需要这个API的Stream。在keySet上使用retainAll。由keySet()返回的Set上的任何更改都反映在原始Map中。

someMap.keySet().retainAll(someList); 
+0

好的,这对我的第二个问题是很好的答案。但我仍然不知道为什么发生java.util.ConcurrentModificationException。 – jaskmar

3

你流呼叫(逻辑)做相同的:

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(); 
    } 
}