web-dev-qa-db-ja.com

Mapsキーが設定されたストリームを使用するとConcurrentModificationException

キーがsomeMapに存在しないsomeListからすべてのアイテムを削除したい。私のコードを見てください:

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

私は受け取ります Java.util.ConcurrentModificationException。どうして?ストリームは並列ではありません。これを行う最もエレガントな方法は何ですか?

21
jaskmar

@Eranはすでに 説明 この問題をよりよく解決する方法。 ConcurrentModificationExceptionが発生する理由を説明します。

ConcurrentModificationExceptionは、ストリームソースを変更するために発生します。 MapHashMapまたはTreeMapまたはその他の非並行マップである可能性があります。 HashMapだとしましょう。すべてのストリームは Spliterator によってサポートされています。 spliteratorにIMMUTABLECONCURRENTの特性がない場合、ドキュメントに次のように記載されています。

Spliteratorをバインドした後、構造的な干渉が検出された場合、ベストエフォートベースでConcurrentModificationExceptionをスローする必要があります。これを行うスプリッターはfail-fastと呼ばれます。

したがって、HashMap.keySet().spliterator()IMMUTABLEではなく(これはSetを変更できるため)、CONCURRENTではありません(同時更新はHashMapに対して安全ではありません) )。そのため、同時に行われる変更を検出し、スプリッターのドキュメントで規定されているようにConcurrentModificationExceptionをスローします。

HashMap のドキュメントも引用する価値があります:

このクラスのすべての「コレクションビューメソッド」によって返されるイテレータはfail-fastです。イテレータの作成後、マップがいつでも構造的に変更された場合、イテレータの独自のremoveメソッドを除いて、イテレータはConcurrentModificationExceptionをスローします。したがって、同時変更に直面した場合、イテレーターは、将来の未確定の時点で恣意的な非決定論的な動作のリスクを冒すのではなく、迅速かつクリーンに失敗します。

イテレータのフェイルファスト動作は、一般的に言えば、非同期の同時変更が存在する場合にハードな保証を行うことが不可能であるため、保証できないことに注意してください。フェイルファストイテレータは、ベストエフォートベースでConcurrentModificationExceptionをスローします。したがって、この例外に正確性を依存するプログラムを作成するのは誤りです。イテレータのフェイルファスト動作は、バグを検出するためだけに使用する必要があります

それはイテレーターについてのみ述べていますが、スプリッターについても同じだと私は信じています。

28
Tagir Valeev

そのためにStream AP​​Iは必要ありません。 retainAllkeySetを使用します。 keySet()によって返されたSetに対する変更は、元のMapに反映されます。

someMap.keySet().retainAll(someList);
13
Eran

あなたのストリーム呼び出しは(論理的に)同じことをしています:

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

後で回答しますが、コレクターをパイプラインに挿入して、forEachがキーのコピーを保持するセットで動作するようにすることができます。

someMap.keySet()
    .stream()
    .filter(v -> !someList.contains(v))
    .collect(Collectors.toSet())
    .forEach(someMap::remove);
3
Ron McLeod