Avatar ·

Удаление объектов из коллекции в цикле без ConcurrentModificationException

Привет всем! Я пишу небольшое приложение на Java для управления списком задач. У меня есть ArrayList, где Task — это простой POJO с полем boolean isCompleted. Мне нужно удалить из списка все выполненные задачи. Я написал такой код: ```java List tasks = new ArrayList<>(); // ... добавляем задачи ... for (Task task : tasks) { if (task.isCompleted()) { tasks.remove(task); } } ``` Но при запуске я получаю `ConcurrentModificationException`. Я читал, что это связано с модификацией коллекции во время итерации, но не понимаю, как правильно это сделать. Пробовал использовать обычный for с индексом — иногда работает, иногда выбрасывает IndexOutOfBoundsException, потому что после удаления индексы сдвигаются. Подскажите, пожалуйста, как правильно удалять элементы из коллекции в цикле? Нужно ли использовать какой-то специальный итератор или есть более современные способы в Java 8+? Заранее спасибо! <<>>

Правильные способы удаления элементов из коллекции во время итерации

Проблема, с которой вы столкнулись, классическая. 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 или итератор.