← Назад к вопросам

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

2.0 Middle🔥 191 комментариев
#Коллекции

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

ConcurrentModificationException и Fail-Fast итераторы

Это один из тех багов, который мне нравится находить в коде junior разработчиков. Fail-Fast поведение — это защитный механизм Java, который помогает избежать тихих ошибок в многопоточной среде.

Какое исключение выбросится

ConcurrentModificationException — это исключение, которое выбросится при нарушении fail-fast гарантии.

List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));

try {
    for (Integer val : list) {
        if (val == 3) {
            list.remove(Integer.valueOf(3));  // Изменяем коллекцию
        }
    }
} catch (ConcurrentModificationException e) {
    System.out.println("Exception: " + e);  // Будет выброшено!
}

Что такое Fail-Fast

Fail-Fast (быстро падай) — это стратегия, когда итератор обнаруживает изменение коллекции и сразу же выбрасывает исключение, вместо того чтобы позволить коду продолжить работу с испорченными данными.

// Как это работает внутри
private class Itr implements Iterator<E> {
    int expectedModCount = modCount;  // Сохраняем версию
    
    public E next() {
        checkForComodification();
        // ...
        return list.get(lastRet);
    }
    
    final void checkForComodification() {
        if (modCount != expectedModCount)  // Проверяем версию
            throw new ConcurrentModificationException();
    }
}

Каждая коллекция имеет счётчик изменений (modCount). Когда итератор создаётся, он сохраняет текущее значение. Если коллекция изменилась — счётчик вырастет, и итератор заметит несоответствие.

Примеры ошибок

1. Удаление элемента во время итерации

List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c", "d"));

// НЕПРАВИЛЬНО! Выбросится ConcurrentModificationException
for (String str : list) {
    if (str.equals("b")) {
        list.remove(str);  // Modifiction!
    }
}

2. Правильный способ 1: используй Iterator.remove()

// ПРАВИЛЬНО! Iterator.remove() не выбрасывает исключение
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    String str = it.next();
    if (str.equals("b")) {
        it.remove();  // Безопасно! Iterator знает об изменении
    }
}

Почему это работает? Потому что Iterator.remove() обновляет expectedModCount:

public void remove() {
    checkForComodification();
    // Удаляем
    list.remove(lastRet);
    expectedModCount = modCount;  // Обновляем версию!
}

3. Правильный способ 2: используй removeIf()

// ПРАВИЛЬНО! removeIf() внутри себя работает безопасно
list.removeIf(str -> str.equals("b"));

4. Правильный способ 3: создай новый список

// ПРАВИЛЬНО! Не трогаем оригинальный список
list = list.stream()
    .filter(str -> !str.equals("b"))
    .collect(Collectors.toList());

ConcurrentHashMap не выбрасывает ConcurrentModificationException

Важное уточнение: ConcurrentHashMap разработан для многопоточной среды и НЕ выбросит исключение:

Map<String, Integer> map = new ConcurrentHashMap<>();
map.put("a", 1);
map.put("b", 2);
map.put("c", 3);

// В ConcurrentHashMap это НЕ выбросит исключение
for (String key : map.keySet()) {
    if (key.equals("b")) {
        map.remove(key);  // Никакого исключения!
    }
}

Почему? Потому что ConcurrentHashMap использует bucketing и synchronization, а не простой счётчик версий.

Многопоточный сценарий

List<Integer> list = Collections.synchronizedList(new ArrayList<>());
list.addAll(Arrays.asList(1, 2, 3, 4, 5));

Thread t1 = new Thread(() -> {
    for (Integer val : list) {
        System.out.println(val);
        Thread.sleep(10);
    }
});

Thread t2 = new Thread(() -> {
    Thread.sleep(25);
    list.remove(Integer.valueOf(3));  // Другой поток изменяет
});

t1.start();
t2.start();
// Возможно ConcurrentModificationException!

Почему это важно

Fail-Fast поведение спасает от тихих ошибок:

// БЕЗ fail-fast (плохо):
for (User user : users) {
    if (user.isInactive()) {
        users.remove(user);  // Молча пропускаем элементы
        // Итератор прыгает через элементы
    }
}
// users перестаёт быть в корректном состоянии

// С fail-fast (хорошо):
for (User user : users) {
    if (user.isInactive()) {
        users.remove(user);  // ConcurrentModificationException!
    }
}
// Немедленно узнаёшь о проблеме!

Практические рекомендации

  1. Используй Iterator.remove() для удаления во время итерации:
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    if (shouldRemove(it.next())) {
        it.remove();
    }
}
  1. Используй removeIf() для фильтрации:
list.removeIf(item -> item.getValue() == 0);
  1. Используй Stream API для трансформаций:
list = list.stream()
    .filter(item -> item.getValue() != 0)
    .collect(Collectors.toList());
  1. Не изменяй коллекцию с другого потока без синхронизации

  2. Если нужна многопоточность, используй ConcurrentHashMap или CopyOnWriteArrayList

ConcurrentModificationException — это не баг, это feature! Он защищает твой код от коварных ошибок.

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