web-dev-qa-db-ja.com

マップを反復して値を変更するときにConcurrentModificationExceptionを回避するにはどうすればよいですか?

いくつかのキー(文字列)と値(POJO)を含むマップを持っています

このマップを繰り返し処理して、POJOの一部のデータを変更します。

私が継承した現在のコードは、指定されたエントリを削除し、POJOにいくつかの変更を加えた後にそれを追加します。

反復処理中にマップを変更するべきではないため、これはうまく機能しません(メソッドは同期されますが、ConcurrentModificationExceptionは引き続き表示されます)。

私の質問は、マップを反復して値を変更する必要がある場合、そのために使用できるベストプラクティス/メソッドは何ですか?別のマップを作成し、それを作成しながら作成するには、コピーを返しますか?

19
Jimmy

2つのオプション:

オプション1

私が継承した現在のコードは、指定されたエントリを削除し、POJOにいくつかの変更を加えた後にそれを追加します。

参照をPOJOに変更していますか?たとえば、エントリは完全に別のものを指しているのですか?そうでない場合は、マップから削除する必要がないため、変更するだけです。

オプション2

doが実際にPOJOへの参照を変更する必要がある場合(たとえば、エントリの値)、_Map.Entry_ entrySet()のインスタンス。エントリでsetValueを使用できます。これにより、反復する対象が変更されることはありません。

例:

_Map<String,String>                  map;
Map.Entry<String,String>            entry;
Iterator<Map.Entry<String,String>>  it;

// Create the map
map = new HashMap<String,String>();
map.put("one", "uno");
map.put("two", "due");
map.put("three", "tre");

// Iterate through the entries, changing one of them
it = map.entrySet().iterator();
while (it.hasNext())
{
    entry = it.next();
    System.out.println("Visiting " + entry.getKey());
    if (entry.getKey().equals("two"))
    {
        System.out.println("Modifying it");
        entry.setValue("DUE");
    }
}

// Show the result
it = map.entrySet().iterator();
while (it.hasNext())
{
    entry = it.next();
    System.out.println(entry.getKey() + "=" + entry.getValue());
}
_

出力(順不同)は次のとおりです。

ふたりを訪ねる
それを修正する
訪問
3人を訪問
2 = DUE
one = uno
three = tre

...変更の例外はありません。他の何かもそのエントリを見て/いじくっている場合に備えて、おそらくこれを同期させたいでしょう。

21
T.J. Crowder

Mapを繰り返し処理し、同時にエントリを追加すると、ほとんどのConcurrentModificationExceptionクラスでMapが生成されます。そして、そうでないMapクラス(たとえば、ConcurrentHashMap)の場合、反復がすべてのエントリを訪問するという保証はありません。

それが何をしているのかに応じて、反復しながら次のことができる場合があります。

  • Iterator.remove()メソッドを使用して現在のエントリを削除する、または
  • Map.Entry.setValue()メソッドを使用して、現在のエントリの値を変更します。

他の種類の変更については、次の操作が必要になる場合があります。

  • 現在のMapのエントリから新しいMapを作成する、または
  • 行われる変更を含む個別のデータ構造を構築し、Mapに適用します。

そして最後に、GoogleコレクションとApache Commonsコレクションライブラリには、マップを「変換」するためのユーティリティクラスがあります。

15
Stephen C

このような目的のために、マップが公開するコレクションビューを使用する必要があります。

  • keySet() を使用すると、キーを反復できます。キーは通常不変であるため、これは役に立ちません。
  • values() は、マップ値にアクセスするだけの場合に必要です。それらが変更可能なオブジェクトである場合は、直接変更できます。マップに戻す必要はありません。
  • entrySet() 最も強力なバージョンで、エントリの値を直接変更できます。

例:大文字を含むすべてのキーの値を大文字に変換する

for(Map.Entry<String, String> entry:map.entrySet()){
    if(entry.getKey().contains("_"))
        entry.setValue(entry.getValue().toUpperCase());
}

実際、値オブジェクトを編集するだけの場合は、値コレクションを使用して編集します。あなたの地図はタイプ<String, Object>

for(Object o: map.values()){
    if(o instanceof MyBean){
        ((Mybean)o).doStuff();
    }
}
8

新しいマップを作成します(mapNew)。次に、既存のマップ(mapOld)を反復処理し、変更および変換されたすべてのエントリをmapNewに追加します。反復が完了したら、mapNewからmapOldまでのすべての値を入力します。ただし、データ量が多い場合は、これでは不十分な場合があります。

または、単に Googleコレクション を使用します-それらにはMaps.transformValues()およびMaps.transformEntries()があります。

3
mindas

適切な答えを提供するために、達成しようとしていることをもう少し説明する必要があります。

それでも、いくつかの(おそらく役立つ)アドバイス:

  • POJOをスレッドセーフにし、POJOのデータを直接更新します。その後、マップを操作する必要はありません。
  • ConcurrentHashMap を使用します
  • simple HashMapを引き続き使用しますが、変更ごとに新しいマップを作成し、シーンの背後でマップを切り替えます(スイッチ操作の同期または AtomicReference

どのアプローチが最適であるかは、アプリケーションに大きく依存します。「ベストプラクティス」を提供することは困難です。いつものように、現実的なデータで独自のベンチマークを作成します。

2
Neeme Praks

ConcurrentHashMap を使用してみてください。

JavaDocから、

取得の完全な同時実行性と更新の予想される同時実行性をサポートするハッシュテーブル。

ConcurrentModificationExceptionが発生するには、通常:

別のスレッドがコレクションを反復している間に、あるスレッドがコレクションを変更することは、一般的に許可されていません。

0
Buhake Sindi

やや拷問される別のアプローチは、マップの値の型として Java.util.concurrent.atomic.AtomicReference を使用することです。あなたの場合、それはあなたのタイプのマップを宣言することを意味します

Map<String, AtomicReference<POJO>>

確かに参照のatomic性質は必要ありませんが、Map.Entry全体を置き換えることなく値スロットを再バインド可能にする安価な方法ですMap#put()

それでも、ここで他のいくつかの応答を読んだので、私は Map.Entry#setValue() の使用をお勧めします。

0
seh