Какое исключение выбрасывается при Fail-Fast поведении в коллекциях Java?

Ответ

При Fail-Fast поведении выбрасывается ConcurrentModificationException.

Почему это происходит? Итераторы большинства стандартных коллекций (например, ArrayList, HashMap) являются fail-fast. Они отслеживают количество структурных модификаций коллекции через внутренний счетчик modCount. Если во время итерации коллекция была изменена не через методы самого итератора (например, напрямую добавлен или удален элемент), счетчики перестают совпадать, и итератор выбрасывает исключение.

Пример, вызывающий исключение:

List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
for (String item : list) {
    if (item.equals("B")) {
        list.remove(item); // ConcurrentModificationException!
    }
}

Как избежать?

  1. Использовать методы самого итератора для удаления:
    Iterator<String> iterator = list.iterator();
    while (iterator.hasNext()) {
        String item = iterator.next();
        if (item.equals("B")) {
            iterator.remove(); // Безопасное удаление
        }
    }
  2. Использовать потокобезопасные коллекции, предназначенные для конкурентного изменения (например, CopyOnWriteArrayList).

Ответ 18+ 🔞

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

Представь, ты идёшь по списку, как по коридору, пальцем тыкаешь: раз, два, три... А в это время какой-то мудак (часто ты сам же, глядя в зеркало) начинает из этого списка элементы выдирать или новые пихать. Коллекция-то меняется, а итератор об этом нихуя не знает! Он как был настроен на старую карту, так и идёт. А там уже ремонт, блядь, стенка новая выросла. Он в неё носом — бац! — и получает свой ConcurrentModificationException прямо в ебло.

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

List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
for (String item : list) {
    if (item.equals("B")) {
        list.remove(item); // ConcurrentModificationException!
    }
}

Ты в цикле for-each (а это под капотом тот же итератор) пытаешься удалить элемент прямо из списка. Это как во время экскурсии по музею начать картины со стен срывать. Смотритель (итератор) охренеет и вышвырнет тебя нахуй с криком «Нарушение правил!».

Так как же не быть мудаком? Есть два основных пути, блядь.

1. Делать всё через официального смотрителя — через сам итератор. У него есть специальный пропуск на удаление.

Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    String item = iterator.next();
    if (item.equals("B")) {
        iterator.remove(); // Безопасное удаление
    }
}

Вот так — тихо, культурно, по-джентльменски. Сказал итератору: «Слушай, дружище, эту хуйню (B) надо убрать». Он кивнул, сам всё аккуратно сделал, внутренние счётчики подправил, и экскурсия продолжается без скандала.

2. Взять специальную, подготовленную коллекцию, которой похуй на изменения. Например, CopyOnWriteArrayList. Эта, блядь, хитрая жопа работает по принципу «пиши в копию». Когда ты итерируешься, ты идешь по старой, неизменной копии списка. А если кто-то его меняет — создаётся свежая копия для следующих операций. Исключения не будет, но и логика может стать немного... неожиданной, если не понимать, как она работает. Зато для многопоточки — иногда то, что надо.

Короче, суть в чём: если используешь итератор — не лезь мимо него в коллекцию руками. Либо используй его методы, либо бери такие коллекции, которые с такими пиздецами справляются. А то получишь исключение в самый неподходящий момент и будешь потом ебаться с дебагом, как дурак.