← Назад к вопросам
Какое исключение выбрасывается при изменении коллекции во время итерации?
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 — это не враг, а помощник, предупреждающий о неправильном использовании коллекции. Всегда слушай это исключение и переделай свой код.