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

Что произойдет если будешь менять коллекцию во время итерирования?

1.7 Middle🔥 171 комментариев
#Коллекции#Основы Java

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

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

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

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

Прямой ответ

Будет выброшено ConcurrentModificationException (в большинстве случаев). Это защитный механизм Java, который предотвращает непредсказуемое поведение при изменении коллекции во время итерирования.

Визуализация проблемы

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

// ❌ ConcurrentModificationException!
for (String item : list) {
    if (item.equals("B")) {
        list.remove(item);  // Изменяем коллекцию во время итерирования
    }
}

// Exception in thread "main" java.util.ConcurrentModificationException

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

Механизм обнаружения через modCount:

// Внутренняя реализация ArrayList
public class ArrayList<E> extends AbstractList<E> {
    protected transient int modCount = 0;  // Счётчик модификаций
    
    @Override
    public boolean add(E e) {
        modCount++;  // Увеличиваем при каждом изменении
        // ... остальной код
    }
    
    @Override
    public boolean remove(Object o) {
        modCount++;  // Увеличиваем и здесь
        // ... остальной код
    }
}

// Итератор проверяет это
private class Itr implements Iterator<E> {
    private int expectedModCount = modCount;  // Запомнили начальное значение
    
    @Override
    public E next() {
        checkForComodification();  // Проверяем перед каждым вызовом
        // ...
    }
    
    private void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();  // БИНГО!
    }
}

Примеры проблемных кодов

Пример 1: Удаление во время for-each

// ❌ ConcurrentModificationException
List<Integer> numbers = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));

for (Integer num : numbers) {
    if (num % 2 == 0) {
        numbers.remove(num);  // Ошибка!
    }
}

Пример 2: Добавление во время итерирования

// ❌ ConcurrentModificationException
List<String> items = new ArrayList<>();
items.add("item1");
items.add("item2");

for (String item : items) {
    if (item.equals("item1")) {
        items.add("item3");  // Добавляем элемент
    }
}

Пример 3: С Iterator явно

// ❌ ConcurrentModificationException
Iterator<String> iterator = list.iterator();

while (iterator.hasNext()) {
    String item = iterator.next();
    if (item.equals("B")) {
        list.remove(item);  // Ошибка! Даже с явным итератором
    }
}

Почему это важно (проблема)

Если бы Java это не проверяла, получались бы баги:

Исходный список: [A, B, C, D, E]
Итератор указывает на индекс 1 (B)

1. Удаляем B → [A, C, D, E]
2. Следующая итерация берёт индекс 2 → получаем D (пропустили C!)

Итог: Пропустили элемент, непредсказуемое поведение

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

Способ 1: Использовать Iterator.remove()

// ✅ Правильно!
Iterator<String> iterator = list.iterator();

while (iterator.hasNext()) {
    String item = iterator.next();
    if (item.equals("B")) {
        iterator.remove();  // Iterator.remove() безопасен
    }
}

Внутренняя реализация:

private class Itr implements Iterator<E> {
    private int lastRet = -1;
    
    @Override
    public void remove() {
        if (lastRet < 0)
            throw new IllegalStateException();
        
        try {
            ArrayList.this.remove(lastRet);
            expectedModCount = modCount;  // Обновляем expected!
            lastRet = -1;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }
}

Способ 2: Создать новый список с отфильтрованными элементами

// ✅ Правильно! (Java 8+)
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
list.removeAll(list.stream()
    .filter(s -> s.equals("B"))
    .collect(Collectors.toList()));

ОР ещё лучше:

// ✅ Самый чистый вариант (Java 8+)
list.removeIf(s -> s.equals("B"));

Способ 3: Collections.synchronizedList (для многопоточности)

// Для многопоточного доступа
List<String> syncList = Collections.synchronizedList(
    new ArrayList<>(Arrays.asList("A", "B", "C"))
);

// Но всё равно нужна синхронизация при итерировании
synchronized (syncList) {
    Iterator<String> iterator = syncList.iterator();
    while (iterator.hasNext()) {
        String item = iterator.next();
        if (item.equals("B")) {
            iterator.remove();  // Безопасно
        }
    }
}

Способ 4: CopyOnWriteArrayList (для многопоточности)

// Для частого чтения, редкого изменения
List<String> list = new CopyOnWriteArrayList<>(
    Arrays.asList("A", "B", "C")
);

// Итерирование не выбросит исключение
for (String item : list) {
    if (item.equals("B")) {
        list.remove(item);  // Не выбросит ConcurrentModificationException
    }
}

// Но это медленнее, потому что копирует весь список при каждом изменении

Полная таблица коллекций

КоллекцияБросает ли ConcurrentModificationException?Решение
ArrayList✓ ДаIterator.remove() или removeIf()
LinkedList✓ ДаIterator.remove() или removeIf()
HashSet✓ ДаIterator.remove() или removeIf()
TreeSet✓ ДаIterator.remove() или removeIf()
HashMap✓ ДаIterator.remove()
Collections.synchronizedList()✓ Да (нужна синхронизация)Iterator.remove() под synchronized
CopyOnWriteArrayList✗ НетБезопасна, но медленнее
ConcurrentHashMap✗ НетБезопасна для итерирования

Реальный пример из production

Задача: Отправить email-ы и удалить отправленные из очереди

// ❌ Плохо
public void sendEmails(List<Email> queue) {
    for (Email email : queue) {
        try {
            emailService.send(email);
            queue.remove(email);  // ConcurrentModificationException!
        } catch (Exception e) {
            log.error("Failed to send", e);
        }
    }
}

// ✅ Хорошо
public void sendEmails(List<Email> queue) {
    Iterator<Email> iterator = queue.iterator();
    while (iterator.hasNext()) {
        Email email = iterator.next();
        try {
            emailService.send(email);
            iterator.remove();  // Безопасное удаление
        } catch (Exception e) {
            log.error("Failed to send", e);
        }
    }
}

// ✅ Или современный вариант
public void sendEmails(List<Email> queue) {
    queue.removeIf(email -> {
        try {
            emailService.send(email);
            return true;  // Удалить из очереди
        } catch (Exception e) {
            log.error("Failed to send", e);
            return false;  // Оставить в очереди
        }
    });
}

Краткий чеклист

НИКОГДА:

  • for (T item : collection) { collection.remove/add(...) }
  • Iterator<T> it = col.iterator(); while (it.hasNext()) { col.remove(...) }

ИСПОЛЬЗУЙ:

  • iterator.remove()
  • collection.removeIf(predicate)
  • collection.removeAll(toRemove)
  • Для многопоточности: CopyOnWriteArrayList или ConcurrentHashMap

Главное правило: Если изменяешь коллекцию, изменяй её через Iterator, а не напрямую через коллекцию.