Как удалять элементы из коллекции в цикле без ConcurrentModificationException в Java?
Проблема: почему возникает 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() или явный итератор.