Как избежать ConcurrentModificationException при переходе по карте и изменении значений?
У меня есть карта, содержащая некоторые ключи (строки) и значения (POJO)
Я хочу выполнить итерацию по этой карте и изменить некоторые данные в POJO.
Текущий код, который я унаследовал, удаляет данную запись и добавляет ее обратно после внесения некоторых изменений в POJO.
Это не сработает, так как вы не должны изменять карту во время ее итерации через нее (метод синхронизирован, но ConcurrentModificationException все еще появляется)
Мой вопрос, если мне нужно перебирать карту и изменять значения, каковы лучшие методы/методы, которые я могу использовать для этого? Чтобы создать отдельную карту и построить ее, когда я иду, верните копию?
Ответы
Ответ 1
Два варианта:
Вариант 1
Текущий код, который я унаследовал, удаляет данную запись и добавляет ее обратно после внесения некоторых изменений в POJO.
Вы меняете ссылку на POJO? Например, поэтому запись указывает на что-то еще полностью? Потому что, если нет, нет необходимости вообще удалять его с карты, вы можете просто изменить его.
Вариант 2
Если вам действительно нужно изменить ссылку на 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());
}
Вывод (в определенном порядке):
Посещение двух
Изменение его
Посещение одного
Посещение трех
два = DUE
один = UNO
три = тр
... без каких-либо изменений. Вероятно, вам захочется синхронизировать это, если что-то еще смотрит на эту запись и сбрасывает ее.
Ответ 2
Итерация по Map
и добавление записей одновременно приведет к ConcurrentModificationException
для большинства классов Map
. А для классов Map
, которые не выполняют (например, ConcurrentHashMap
), нет гарантии, что итерация посетит все записи.
В зависимости от того, что именно вы делаете, вы можете выполнять следующие операции при повторении:
- используйте метод
Iterator.remove()
для удаления текущей записи или
- используйте метод
Map.Entry.setValue()
для изменения текущего значения записи.
Для других типов изменений вам может потребоваться:
- создайте новый
Map
из записей текущего Map
или
- создайте отдельную структуру данных, содержащую изменения, которые необходимо внести, затем примените к
Map
.
И, наконец, библиотеки коллекций Google и Apache Commons Collections имеют классы утилиты для "преобразования" карт.
Ответ 3
Для таких целей вы должны использовать наборы представлений, которые предоставляет карта:
- 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();
}
}
Ответ 4
Создайте новую карту (mapNew). Затем перейдите по существующей карте (mapOld) и добавьте все измененные и преобразованные записи в mapNew. По завершении итерации поместите все значения из mapNew в mapOld. Это может быть недостаточно, если количество данных велико.
Или просто используйте коллекции Google - у них есть Maps.transformValues()
и Maps.transformEntries()
.
Ответ 5
Чтобы обеспечить правильный ответ, вы должны объяснить немного больше, чего вы пытаетесь достичь.
Тем не менее, некоторые (возможно полезные) советы:
- сделать POJO потокобезопасным и делать обновления данных на POJO напрямую. Тогда вам не нужно манипулировать картой.
- использовать ConcurrentHashMap
- продолжайте использовать простой HashMap, но создавайте новую карту на каждой модификации и переключайте карты за кулисами (синхронизируя работу переключателя или используя AtomicReference)
Какой подход лучше всего зависит от вашего приложения, трудно дать вам "наилучшую практику". Как всегда, создайте собственный тест с реалистичными данными.
Ответ 6
Попробуйте использовать ConcurrentHashMap.
Из JavaDoc,
Хеш-таблица, поддерживающая полный concurrency извлечения и регулируемый ожидаемый concurrency для обновления.
Для возникновения ConcurrentModificationException обычно:
в целом это недопустимо для один поток для изменения коллекции в то время как другой поток выполняет итерацию он.
Ответ 7
Другой подход, несколько замученный, заключается в использовании java.util.concurrent.atomic.AtomicReference
в качестве типа вашего значения карты. В вашем случае это означает объявление вашей карты типа
Map<String, AtomicReference<POJO>>
Вам, разумеется, не нужна атомная природа ссылки, но это дешевый способ сделать слоты значений перегруппированными, не заменяя весь Map.Entry
на Map#put()
.
Однако, прочитав некоторые другие ответы здесь, я также рекомендую использовать Map.Entry#setValue()
, который мне никогда не нужен и не заметил до тех пор, пока сегодня.