Какое исключение выбрасывается при изменении коллекции во время итерации по ней в Java?

Ответ

При изменении коллекции во время итерации по ней (не через методы итератора) выбрасывается ConcurrentModificationException.

Механизм: Это часть контракта fail-fast итераторов. Итератор проверяет, что внутренний счетчик модификаций коллекции (modCount) не изменился с момента его создания. Любое структурное изменение коллекции (добавление, удаление) увеличивает этот счетчик. Если итератор обнаруживает расхождение, он немедленно выбрасывает исключение.

Пример возникновения:

List<Integer> numbers = new ArrayList<>();
numbers.add(1);
numbers.add(2);

for (Integer num : numbers) { // Неявно создается итератор
    if (num == 1) {
        numbers.add(3); // Прямое изменение коллекции -> Исключение!
    }
}

Способы безопасного изменения во время итерации:

  1. Использовать Iterator.remove():
    Iterator<Integer> it = numbers.iterator();
    while (it.hasNext()) {
        Integer num = it.next();
        if (num == 1) {
            it.remove(); // Безопасно
        }
    }
  2. Создать копию коллекции для итерации: Итерироваться по копии, изменять оригинал.
  3. Использовать CopyOnWriteArrayList: Эта коллекция создает новую копию внутреннего массива при каждой модификации, что позволяет безопасно итерироваться по старой копии.

Ответ 18+ 🔞

А, слушай, вот эта ваша ConcurrentModificationException — это вообще классика, пиздец! Прямо как в анекдоте: "Нельзя так делать, но если очень хочется, то можно, но потом охуеешь".

Представь картину, блядь. Ты спокойно идёшь по списку, как по магазину, смотришь на элементы. А тут твоя же рука, ёпта, из другого кармана, начинает в этот самый список хуярить новые штуки или выкидывать старые. Коллекция, сука, в шоке: "Мужик, ты чё, блядь? Я уже считаюсь, а ты тут меня ломаешь на ходу!" И бац — тебе в ебальник летит ConcurrentModificationException. Это как будто тебе говорят: "Чувак, блядь, определись — либо ты меня перебираешь, либо меняешь. Одновременно — нихуя!"

Вот смотри, самый идиотский пример, который все делают:

List<Integer> numbers = new ArrayList<>();
numbers.add(1);
numbers.add(2);

for (Integer num : numbers) { // Тут внутри невидимый итератор уже создался, блядь
    if (num == 1) {
        numbers.add(3); // АХТУНГ! Пиздец! Прямое вмешательство в процесс!
    }
}

Запустишь — получишь по ебалу исключением. Итератор, сука, как сторожевой пёс: он запомнил, сколько раз коллекцию трогали (modCount), а тут бац — количество изменилось без его ведома. Он такой: "Нахуй! Измена! Всё, я сваливаю!" — и кидает тебе ошибку.

Так как же, блядь, делать-то правильно, если очень надо?

  1. Через самого итератора. У него есть свой законный метод remove(). Это как сказать: "Слушай, я сейчас через тебя работаю, вот этот элемент — нахуй его".

    Iterator<Integer> it = numbers.iterator();
    while (it.hasNext()) {
        Integer num = it.next();
        if (num == 1) {
            it.remove(); // Всё легально, итератор сам всё согласует
        }
    }

    С add через обычный Iterator сложнее, его там нет. Для этого уже другие итераторы нужны.

  2. Сделать копию, блядь. Идея проще некуда: итерируешься по одной копии коллекции, а хуяришь в другую (или в оригинал). Они же разные, сука, объекты — итератору на оригинал похуй, что ты творишь с копией.

  3. Взять CopyOnWriteArrayList. Это, блядь, специальная коллекция для таких распиздяев, которые любят менять на лету. Её принцип — "пиши, сука, в копию". Когда ты её меняешь, она создаёт полную новую копию внутреннего массива. А старый итератор работает со старой, неизменной копией. Удобно, но дорого по памяти, если изменения частые — копии будут лететь, как хуи с горы.

Короче, суть в чём: Java тебя страхует от самого себя, от кривых race condition в одном потоке. Не лезь, блядь, с ногами в тазик, пока в нём стираешь. Либо стирай, либо ноги мой. Или купи два тазика, ёпта.