キーが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をバインドした後、構造的な干渉が検出された場合、ベストエフォートベースで
ConcurrentModificationException
をスローする必要があります。これを行うスプリッターはfail-fastと呼ばれます。
したがって、HashMap.keySet().spliterator()
はIMMUTABLE
ではなく(これはSet
を変更できるため)、CONCURRENT
ではありません(同時更新はHashMap
に対して安全ではありません) )。そのため、同時に行われる変更を検出し、スプリッターのドキュメントで規定されているようにConcurrentModificationException
をスローします。
HashMap
のドキュメントも引用する価値があります:
このクラスのすべての「コレクションビューメソッド」によって返されるイテレータはfail-fastです。イテレータの作成後、マップがいつでも構造的に変更された場合、イテレータの独自のremoveメソッドを除いて、イテレータは
ConcurrentModificationException
をスローします。したがって、同時変更に直面した場合、イテレーターは、将来の未確定の時点で恣意的な非決定論的な動作のリスクを冒すのではなく、迅速かつクリーンに失敗します。イテレータのフェイルファスト動作は、一般的に言えば、非同期の同時変更が存在する場合にハードな保証を行うことが不可能であるため、保証できないことに注意してください。フェイルファストイテレータは、ベストエフォートベースで
ConcurrentModificationException
をスローします。したがって、この例外に正確性を依存するプログラムを作成するのは誤りです。イテレータのフェイルファスト動作は、バグを検出するためだけに使用する必要があります。
それはイテレーターについてのみ述べていますが、スプリッターについても同じだと私は信じています。
そのためにStream
APIは必要ありません。 retainAll
でkeySet
を使用します。 keySet()
によって返されたSet
に対する変更は、元のMap
に反映されます。
someMap.keySet().retainAll(someList);
あなたのストリーム呼び出しは(論理的に)同じことをしています:
for (K k : someMap.keySet()) {
if (!someList.contains(k)) {
someMap.remove(k);
}
}
これを実行すると、ConcurrentModificationException
がスローされることがわかります。これは、マップを繰り返し処理すると同時にマップを変更しているためです。 docs を見ると、次のことがわかります。
この例外は、オブジェクトが別のスレッドによって同時に変更されたことを常に示すわけではないことに注意してください。単一のスレッドがオブジェクトのコントラクトに違反する一連のメソッド呼び出しを発行すると、オブジェクトはこの例外をスローする場合があります。たとえば、スレッドがフェイルファストイテレータでコレクションを反復処理しているときに、スレッドがコレクションを直接変更した場合、イテレータはこの例外をスローします。
これはあなたがやっていることです、あなたが使用しているマップ実装は明らかにフェイルファストイテレータを持っているので、この例外がスローされています。
考えられる代替策の1つは、イテレーターを直接使用してアイテムを削除することです。
for (Iterator<K> ks = someMap.keySet().iterator(); ks.hasNext(); ) {
K next = ks.next();
if (!someList.contains(k)) {
ks.remove();
}
}
後で回答しますが、コレクターをパイプラインに挿入して、forEachがキーのコピーを保持するセットで動作するようにすることができます。
someMap.keySet()
.stream()
.filter(v -> !someList.contains(v))
.collect(Collectors.toSet())
.forEach(someMap::remove);