← Назад к вопросам

Какое исключение выбрасывается при изменении коллекции во время итерации?

2.0 Middle🔥 71 комментариев
#Коллекции#Многопоточность

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

ConcurrentModificationException при изменении коллекции во время итерации

ConcurrentModificationException — это исключение, которое выбрасывается когда коллекция изменяется во время итерирования, за исключением случаев, когда изменение делается через сам итератор. Это механизм защиты от скрытых багов.

Основной механизм: modCount

Основной объект контролирует modCount (счётчик модификаций):

public class ArrayList<E> extends AbstractList<E> {
    protected transient int modCount = 0;  // Счётчик изменений
    
    public boolean add(E e) {
        modCount++;  // Увеличиваем при добавлении
        // ...
    }
    
    public boolean remove(Object o) {
        modCount++;  // Увеличиваем при удалении
        // ...
    }
    
    public Iterator<E> iterator() {
        return new Itr();  // Iterator запоминает текущий modCount
    }
    
    private class Itr implements Iterator<E> {
        int expectedModCount = modCount;  // Снимок в момент создания
        
        public boolean hasNext() {
            checkForComodification();  // Проверяем перед каждой операцией
            // ...
        }
        
        public E next() {
            checkForComodification();  // Проверяем
            // ...
        }
        
        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }
}

Классический пример проблемы

List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");

// ✗ НЕПРАВИЛЬНО: изменяем коллекцию во время итерации
for (String item : list) {
    System.out.println(item);
    list.remove(item);  // ConcurrentModificationException!
}

// Почему происходит:
// 1. for-each синтаксис использует Iterator внутренне
// 2. Iterator запомнил modCount = 3 (было 3 add операции)
// 3. list.remove() увеличивает modCount на 4
// 4. На следующей итерации: checkForComodification() выбросит исключение

Почему это происходит

// Развёрнутый код за for-each:
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");

Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    String item = iterator.next();  // Внутри проверяет modCount
    System.out.println(item);
    list.remove(item);  // modCount увеличилась!
    // На следующей итерации:
    // iterator.hasNext() выбросит ConcurrentModificationException
}

Правильные способы удаления во время итерации

1. Использовать iterator.remove()

Самый безопасный способ:

List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");

Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    String item = iterator.next();
    if (item.equals("B")) {
        iterator.remove();  // ПРАВИЛЬНО! Обновляет expectedModCount
    }
}

// Результат: [A, C]

// Внутри iterator.remove():
// 1. Удаляет элемент из коллекции (modCount++)
// 2. Обновляет expectedModCount (expectedModCount = modCount)
// 3. Нет исключения!

2. Использовать removeIf() (Java 8+)

List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");

// Удаляет все элементы, соответствующие предикату
list.removeIf(item -> item.equals("B"));
// Результат: [A, C]

// Внутри removeIf() правильно обрабатывает modCount

3. Stream API (функциональный подход)

List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");

// Создаёт новую коллекцию без "B"
list = list.stream()
        .filter(item -> !item.equals("B"))
        .collect(Collectors.toList());
// Результат: [A, C]

4. Создать копию для итерации

List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");

// Итерируем копию, удаляем из оригинала
for (String item : new ArrayList<>(list)) {
    if (item.equals("B")) {
        list.remove(item);  // ПРАВИЛЬНО! Итератор работает с копией
    }
}
// Результат: [A, C]

5. Использовать List.removeAll()

List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");

List<String> toRemove = Arrays.asList("B");
list.removeAll(toRemove);  // Атомично удаляет все
// Результат: [A, C]

Стек-трейс типичного исключения

Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:912)
    at java.util.ArrayList$Itr.next(ArrayList.java:858)
    at TestClass.main(TestClass.java:15)

// Строка 15 — это call к iterator.next()
// Он вызвал checkForComodification()
// Который обнаружил, что modCount изменился

Пример с добавлением (тоже запрещено)

List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");

// ✗ НЕПРАВИЛЬНО: добавляем во время итерации
for (String item : list) {
    System.out.println(item);
    list.add("D");  // ConcurrentModificationException!
}

Многопоточная ситуация (реальная конкурентность)

List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");

// Поток 1: итерирует
Thread t1 = new Thread(() -> {
    try {
        for (String item : list) {
            System.out.println(item);
            Thread.sleep(100);
        }
    } catch (ConcurrentModificationException e) {
        System.out.println("List was modified by another thread!");
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
});

// Поток 2: изменяет список
Thread t2 = new Thread(() -> {
    try {
        Thread.sleep(50);
        list.add("D");  // Изменяет во время итерации t1
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
});

t1.start();
t2.start();
t1.join();
t2.join();

// t1 выбросит ConcurrentModificationException

Защита для многопоточности: synchronized коллекции

// ✗ ArrayList НЕ потокобезопасен
List<String> list = new ArrayList<>();

// ✓ Collections.synchronizedList() добавляет синхронизацию
List<String> syncList = Collections.synchronizedList(new ArrayList<>());

// Но это НЕ решает проблему итерации:
try {
    for (String item : syncList) {
        syncList.remove(item);  // ВСЕ РАВНО ConcurrentModificationException!
    }
} catch (ConcurrentModificationException e) {
    // Происходит потому что:
    // synchronized защищает от race conditions
    // но не от этой логической ошибки
}

// ПРАВИЛЬНО для synchronized коллекций:
synchronized(syncList) {
    Iterator<String> it = syncList.iterator();
    while (it.hasNext()) {
        if (it.next().equals("B")) {
            it.remove();  // ПРАВИЛЬНО
        }
    }
}

CopyOnWriteArrayList (нет исключения)

// CopyOnWriteArrayList НЕ выбрасывает ConcurrentModificationException
// потому что iterator работает со снимком

CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("A");
list.add("B");
list.add("C");

for (String item : list) {
    System.out.println(item);
    list.add("D");  // БЕЗ ИСКЛЮЧЕНИЯ!
    list.remove("B");  // БЕЗ ИСКЛЮЧЕНИЯ!
}
// Результат в консоли: A, B, C
// (iterator видит старый снимок [A, B, C])

LinkedList: особенности

List<String> list = new LinkedList<>();
list.add("A");
list.add("B");
list.add("C");

// LinkedList тоже выбрасывает ConcurrentModificationException
try {
    for (String item : list) {
        list.remove(item);
    }
} catch (ConcurrentModificationException e) {
    // LinkedList использует то же механизм modCount
}

// Но LinkedListIterator.remove() работает:
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    if (it.next().equals("B")) {
        it.remove();  // ПРАВИЛЬНО
    }
}

ConcurrentHashMap (многопоточность, но нет исключения)

// ConcurrentHashMap позволяет модификации во время итерации
Map<String, Integer> map = new ConcurrentHashMap<>();
map.put("A", 1);
map.put("B", 2);
map.put("C", 3);

for (String key : map.keySet()) {
    System.out.println(key);
    map.put("D", 4);  // БЕЗ ИСКЛЮЧЕНИЯ!
    map.remove("B");  // БЕЗ ИСКЛЮЧЕНИЯ!
}
// Iterator видит изменения (или нет, зависит от timing)

Практические советы

// ✓ ПРАВИЛЬНО: iterator.remove()
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    String item = it.next();
    if (shouldRemove(item)) {
        it.remove();
    }
}

// ✓ ПРАВИЛЬНО: removeIf()
list.removeIf(item -> shouldRemove(item));

// ✓ ПРАВИЛЬНО: Stream API
list = list.stream()
    .filter(item -> !shouldRemove(item))
    .collect(Collectors.toList());

// ✓ ПРАВИЛЬНО: копия для итерации
for (String item : new ArrayList<>(list)) {
    if (shouldRemove(item)) {
        list.remove(item);
    }
}

// ✗ НЕПРАВИЛЬНО: изменение коллекции напрямую
for (String item : list) {
    list.remove(item);  // ConcurrentModificationException!
}

Сравнение коллекций

КоллекцияВыбрасывает CMEРешение
ArrayList✓ Даiterator.remove(), removeIf(), Stream
LinkedList✓ Даiterator.remove(), removeIf(), Stream
HashSet✓ Даiterator.remove(), Stream
TreeSet✓ Даiterator.remove(), Stream
HashMap✓ Да (на keySet/values)iterator.remove(), Stream
CopyOnWriteArrayList✗ НетМожно изменять, но iterator видит снимок
ConcurrentHashMap✗ Нет (слабо)Можно изменять, но с осторожностью

Почему это нужно

Этот механизм (ConcurrentModificationException) помогает обнаружить:

  • Логические ошибки: случайное изменение коллекции
  • Race conditions: несинхронизированный доступ из разных потоков
  • Скрытые баги: которые проявляются недетерминированно

Заключение: ConcurrentModificationException — это не враг, а помощник, предупреждающий о неправильном использовании коллекции. Всегда слушай это исключение и переделай свой код.