Что такое ConcurrentModificationException в Java и как его избежать?

«Что такое ConcurrentModificationException в Java и как его избежать?» — вопрос из категории Java Core, который задают на 10% собеседований Java Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

ConcurrentModificationException — это unchecked исключение, которое возникает при изменении структуры коллекции (добавление, удаление элементов) во время её итерации любым способом, кроме использования методов самого итератора.

Причина: Большинство коллекций java.util (кроме потокобезопасных) имеют fail-fast итераторы, которые проверяют, не изменилась ли коллекция с момента начала итерации (по счётчику модификаций). При обнаружении изменения выбрасывается это исключение.

Пример, вызывающий исключение:

List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
for (String item : list) { // Неявно использует Iterator
    if ("B".equals(item)) {
        list.remove(item); // Модификация коллекции во время итерации!
    }
}

Способы безопасного удаления элементов во время итерации:

  1. Использование метода Iterator.remove():

    Iterator<String> iterator = list.iterator();
    while (iterator.hasNext()) {
        String item = iterator.next();
        if ("B".equals(item)) {
            iterator.remove(); // Корректный способ удаления
        }
    }
  2. Использование removeIf() (Java 8+):

    list.removeIf(item -> "B".equals(item));
  3. Сбор элементов для удаления в отдельную коллекцию:

    List<String> toRemove = new ArrayList<>();
    for (String item : list) {
        if ("B".equals(item)) {
            toRemove.add(item);
        }
    }
    list.removeAll(toRemove);
  4. Использование потокобезопасных коллекций (для многопоточного доступа):

    • CopyOnWriteArrayList — создаёт копию массива при изменении, итерация идёт по снимку на момент создания итератора.
    • ConcurrentHashMap — позволяет безопасные модификации во время итерации (итератор может отражать не все последние изменения).