Avatar ·

Как удалять элементы из коллекции в цикле без ConcurrentModificationException в Java?

Всем привет! Я начинающий Java-разработчик и столкнулся с очень странной ошибкой. Пишу простую программу: есть список строк, и я хочу удалить из него все элементы, которые начинаются с буквы "A". Вроде бы всё логично, но при запуске вылетает ConcurrentModificationException. Вот мой код: ```java List list = new ArrayList<>(Arrays.asList("Apple", "Banana", "Avocado", "Cherry")); for (String fruit : list) { if (fruit.startsWith("A")) { list.remove(fruit); } } ``` Ошибка возникает на итерации после удаления. Я читал, что нельзя модифицировать коллекцию во время for-each цикла, но как тогда правильно удалить элементы? Мне нужно именно изменить исходный список, а не создавать новый. Подскажите, пожалуйста, как это сделать правильно? Какие есть способы безопасного удаления элементов из коллекции во время её обхода? Заранее спасибо! <<>>

Проблема: почему возникает ConcurrentModificationException

Вы столкнулись с классической проблемой Java. Когда вы используете for-each цикл (или итератор неявно), итератор отслеживает количество изменений в коллекции. При вызове list.remove() напрямую через список, внутренний счётчик изменений итератора (modCount) расходится с ожидаемым, и итератор выбрасывает исключение при следующей попытке получить элемент.

Решение 1: Использование Iterator (рекомендуемый способ)

Самый правильный и безопасный способ — использовать явный Iterator и его метод remove(). Итератор знает о модификациях и корректно обновляет своё состояние.


List list = new ArrayList<>(Arrays.asList("Apple", "Banana", "Avocado", "Cherry"));
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
    String fruit = iterator.next();
    if (fruit.startsWith("A")) {
        iterator.remove(); // Безопасное удаление через итератор
    }
}
// Результат: [Banana, Cherry]

Решение 2: Использование removeIf() (Java 8+)

Начиная с Java 8, у коллекций есть встроенный метод removeIf(), который делает то же самое в одну строку. Это самый лаконичный вариант.


List list = new ArrayList<>(Arrays.asList("Apple", "Banana", "Avocado", "Cherry"));
list.removeIf(fruit -> fruit.startsWith("A"));
// Результат: [Banana, Cherry]

Решение 3: Обратный цикл по индексу

Если вы работаете со списком (List), можно идти с конца в начало. При удалении элемента с конца индексы предыдущих элементов не сдвигаются, поэтому цикл остаётся корректным.


List list = new ArrayList<>(Arrays.asList("Apple", "Banana", "Avocado", "Cherry"));
for (int i = list.size() - 1; i >= 0; i--) {
    if (list.get(i).startsWith("A")) {
        list.remove(i);
    }
}
// Результат: [Banana, Cherry]

Решение 4: Создание нового списка (для иммутабельности)

Иногда проще не удалять, а собрать нужные элементы в новый список. Это не модифицирует исходный, но часто является более чистым подходом с точки зрения функционального программирования.


List list = new ArrayList<>(Arrays.asList("Apple", "Banana", "Avocado", "Cherry"));
List filteredList = new ArrayList<>();
for (String fruit : list) {
    if (!fruit.startsWith("A")) {
        filteredList.add(fruit);
    }
}
// filteredList: [Banana, Cherry]

Что НЕ РАБОТАЕТ

  • For-each с list.remove() — вызывает ConcurrentModificationException.
  • Обычный for с list.remove(i) в прямом порядке — после удаления индексы сдвигаются, и вы пропускаете элементы.
  • Использование list.remove() внутри stream().forEach() — тоже приведёт к исключению.

Рекомендация

Для Java 8+ используйте removeIf() — это идиоматично и безопасно. Если вам нужно больше контроля (например, удаление по сложному условию с дополнительной логикой), используйте явный Iterator. Обратный цикл по индексу — это fallback для случаев, когда итератор недоступен (например, для массивов).

<<>>

Альтернативный подход: Использование CopyOnWriteArrayList

Если вам нужно модифицировать коллекцию во время итерации, и вы не хотите думать о синхронизации, можно использовать потокобезопасную коллекцию CopyOnWriteArrayList. Она создаёт новую копию массива при каждой модификации, поэтому итератор никогда не видит изменений и не выбрасывает исключений.


import java.util.concurrent.CopyOnWriteArrayList;

List list = new CopyOnWriteArrayList<>(Arrays.asList("Apple", "Banana", "Avocado", "Cherry"));
for (String fruit : list) {
    if (fruit.startsWith("A")) {
        list.remove(fruit);
    }
}
// Результат: [Banana, Cherry]

Плюсы:

  • Можно использовать обычный for-each без итератора.
  • Потокобезопасен для чтения.

Минусы:

  • Низкая производительность при частых модификациях (каждое добавление/удаление копирует весь массив).
  • Не подходит для больших коллекций или операций с частыми изменениями.

Этот подход оправдан, когда коллекция редко изменяется, но часто читается (например, кеш конфигураций). Для обычных сценариев лучше использовать removeIf() или явный итератор.