Удаление объектов из коллекции в цикле без ConcurrentModificationException
Правильные способы удаления элементов из коллекции во время итерации
Проблема, с которой вы столкнулись, классическая. ConcurrentModificationException возникает, когда вы изменяете коллекцию (добавляете или удаляете элементы) во время прохода по ней с помощью for-each или итератора, не используя методы самого итератора. Рассмотрим несколько правильных подходов.
1. Использование Iterator и его метода remove() (Java 5+)
Это классический и надёжный способ. Итератор «знает» о текущей позиции и корректно обновляет свои внутренние счётчики при удалении.
Iterator<Task> iterator = tasks.iterator();
while (iterator.hasNext()) {
Task task = iterator.next();
if (task.isCompleted()) {
iterator.remove(); // Безопасное удаление через итератор
}
}
2. Использование Collection.removeIf() (Java 8+)
Самый современный и лаконичный способ. Метод removeIf принимает предикат (лямбда-выражение) и удаляет все элементы, удовлетворяющие условию. Внутри он сам использует итератор безопасно.
tasks.removeIf(Task::isCompleted); // Или: tasks.removeIf(task -> task.isCompleted());
3. Обычный for с ручной корректировкой индекса
Если вы используете обычный цикл for по индексу, нужно не забывать уменьшать индекс после удаления, так как элементы сдвигаются влево.
for (int i = 0; i < tasks.size(); i++) {
if (tasks.get(i).isCompleted()) {
tasks.remove(i);
i--; // Важно! Компенсируем сдвиг
}
}
4. Использование Stream API (Java 8+) для создания новой коллекции
Если вам не обязательно модифицировать исходный список, можно создать новый, отфильтрованный. Это особенно полезно для иммутабельных коллекций.
List<Task> filteredTasks = tasks.stream()
.filter(task -> !task.isCompleted()) // Оставляем только невыполненные
.collect(Collectors.toList());
Почему возникает ConcurrentModificationException?
Когда вы используете for-each (for (Task task : tasks)), компилятор неявно создаёт итератор. При вызове tasks.remove(task) вы изменяете саму коллекцию, а не итератор. Итератор при следующем вызове next() проверяет, не изменилась ли коллекция «сбоку», и если да — бросает исключение. Метод iterator.remove() синхронизирует состояние итератора с коллекцией, поэтому исключения не возникает.
Рекомендация
Для Java 8 и выше используйте removeIf — это самый короткий, читаемый и эффективный способ. Для более старых версий Java — явный итератор. Избегайте ручного управления индексами, если это не критично для производительности.
Альтернативный подход: работа с копией коллекции
Если по каким-то причинам вы не хотите или не можете использовать Iterator или removeIf, можно создать копию коллекции для итерации, а удалять из оригинала. Это решает проблему, так как итератор проходит по копии, а изменения вносятся в оригинал.
// Создаём копию для итерации
List<Task> copy = new ArrayList<>(tasks);
for (Task task : copy) {
if (task.isCompleted()) {
tasks.remove(task); // Удаляем из оригинала
}
}
Этот способ прост для понимания, но имеет недостатки:
- Дополнительная память: Создаётся полная копия списка, что может быть проблематично для больших коллекций.
- Производительность: Удаление из ArrayList по объекту (не по индексу) работает за O(n), а если удаляется много элементов, общая сложность может быть квадратичной.
- Проблемы с equals/hashCode: Метод
remove(Object)используетequals(), поэтому убедитесь, что у классаTaskправильно переопределён этот метод.
Этот подход стоит применять, когда коллекция небольшая, или когда вы не можете менять сигнатуру метода (например, работаете с устаревшим кодом). В остальных случаях лучше использовать removeIf или итератор.