Почему генерация исключений при модификации коллекции во время итерации усложняет код?

«Почему генерация исключений при модификации коллекции во время итерации усложняет код?» — вопрос из категории Other, который задают на 10% собеседований Java Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Такое поведение (например, ConcurrentModificationException в Java) усложняет разработку, потому что нарушает принцип наименьшего удивления и требует от программиста явного управления структурными изменениями коллекции, что не всегда очевидно.

Основные проблемы:

  1. Скрытые побочные эффекты: Исключение может быть выброшено в неожиданном месте, если коллекция передается между методами.
  2. Усложнение логики: Код должен либо избегать структурных изменений во время итерации, либо использовать специальные механизмы, что увеличивает когнитивную нагрузку.
  3. Сложность отладки: Исключение часто возникает далеко от места реальной ошибки — модификации коллекции.

Пример проблемы и решения:

// ❌ ПРОБЛЕМА: ConcurrentModificationException
List<String> list = new ArrayList<>(List.of("A", "B", "C"));
for (String item : list) { // Неявно создается Iterator
    if ("B".equals(item)) {
        list.remove(item); // Прямое изменение исходной коллекции!
    }
}

// ✅ РЕШЕНИЕ 1: Использовать явный Iterator и его метод remove()
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    if ("B".equals(it.next())) {
        it.remove(); // Безопасное удаление через итератор
    }
}

// ✅ РЕШЕНИЕ 2: Использовать потокобезопасные коллекции (например, из java.util.concurrent)
CopyOnWriteArrayList<String> safeList = new CopyOnWriteArrayList<>(list);
for (String item : safeList) { // Итератор работает по снимку (snapshot) на момент создания
    if ("B".equals(item)) {
        safeList.remove(item); // Допустимо, но создает новую внутреннюю копию
    }
}

// ✅ РЕШЕНИЕ 3: Собрать элементы для удаления, затем удалить их после итерации
List<String> toRemove = new ArrayList<>();
for (String item : list) {
    if (item.startsWith("B")) {
        toRemove.add(item);
    }
}
list.removeAll(toRemove);

Вывод: Необходимость обрабатывать ConcurrentModificationException заставляет явно проектировать алгоритм обхода и модификации, что увеличивает объем и сложность кода.