2015-09-15 104 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(); 
    } 
}