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

Что такое fail-fast и fail-safe итераторы в Java?

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

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

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

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

# Fail-Fast и Fail-Safe итераторы в Java

Определение

Fail-Fast итератор — выбросит ConcurrentModificationException, если коллекция изменена во время итерации (кроме как через сам итератор).

Fail-Safe итератор — работает с копией коллекции, поэтому изменения во время итерации безопасны.

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

Как работают

List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");

// Fail-fast в действии
for (String item : list) {
    if (item.equals("B")) {
        list.remove(item);  // ❌ Выбросит ConcurrentModificationException
    }
}

Механизм:

  • ArrayList, HashMap, HashSet используют внутренний счётчик modCount
  • Каждый раз, когда коллекция изменяется (add, remove, clear), modCount увеличивается
  • Итератор помнит значение modCount при создании
  • На каждой итерации проверяет: текущий modCount == сохранённый modCount?
  • Если нет → ConcurrentModificationException

Реализация в коде

// Упрощённое представление ArrayList
private int modCount = 0;

public Iterator<E> iterator() {
    return new Itr();
}

private class Itr implements Iterator<E> {
    int expectedModCount = modCount;
    
    public boolean hasNext() {
        return cursor != size();
    }
    
    public E next() {
        checkForComodification();  // Проверка!
        return list.get(cursor++);
    }
    
    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}

public boolean remove(Object o) {
    modCount++;  // Счётчик изменился
    // ... удаление элемента
}

Какие коллекции fail-fast

  • ArrayList
  • HashMap
  • HashSet
  • LinkedList
  • TreeMap
  • TreeSet

Правильное удаление

List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));

// ✅ Вариант 1: использовать итератор
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    String item = it.next();
    if (item.equals("B")) {
        it.remove();  // Безопасно — обновляет expectedModCount
    }
}

// ✅ Вариант 2: использовать stream
list = list.stream()
    .filter(item -> !item.equals("B"))
    .collect(Collectors.toList());

// ✅ Вариант 3: removeIf
list.removeIf(item -> item.equals("B"));

Fail-Safe итераторы

Как работают

List<String> list = new CopyOnWriteArrayList<>();
list.add("A");
list.add("B");
list.add("C");

// Fail-safe — работает!
for (String item : list) {
    if (item.equals("B")) {
        list.remove(item);  // ✅ Не выбросит исключение
    }
}

Механизм:

  • CopyOnWriteArrayList создаёт копию массива при создании итератора
  • Итератор работает с копией, оригинал может изменяться
  • Новые потоки будут видеть новые данные, но текущий итератор — старые

Реализация

public Iterator<E> iterator() {
    // Создаёт снимок текущего состояния
    return new COWIterator<>(getArray(), 0);
}

private static class COWIterator<E> {
    private final Object[] snapshot;  // Копия данных
    private int cursor = 0;
    
    COWIterator(Object[] snapshot, int initialCursor) {
        this.snapshot = snapshot;
        this.cursor = initialCursor;
    }
    
    public boolean hasNext() {
        return cursor < snapshot.length;
    }
    
    public E next() {
        return (E) snapshot[cursor++];  // Читает из копии
    }
}

Какие коллекции fail-safe

  • CopyOnWriteArrayList
  • ConcurrentHashMap (частично)
  • ConcurrentSkipListMap

Сравнение

АспектFail-FastFail-Safe
Изменение во время итерацииConcurrentModificationExceptionРаботает (но видит старые данные)
Производительность итерацииБыстроЧуть медленнее (из-за копии)
Производительность writeБыстроМедленно (копирует весь массив)
ПотокобезопасностьНетДа (CopyOnWriteArrayList)
ПамятьМинимумДополнительная память на копию
ИспользуетсяПо умолчаниюКогда нужна потокобезопасность

Практические примеры

Проблема fail-fast

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

// ❌ Выбросит исключение
try {
    for (Integer num : numbers) {
        if (num % 2 == 0) {
            numbers.remove(num);
        }
    }
} catch (ConcurrentModificationException e) {
    System.out.println("Ошибка при удалении во время итерации");
}

Решение с fail-safe

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

// ✅ Работает
for (Integer num : numbers) {
    if (num % 2 == 0) {
        numbers.remove(num);  // Безопасно
    }
}
// Но итератор уже видел все элементы из копии

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

// CopyOnWriteArrayList для высокой контенции читаемых операций
List<String> subscribers = new CopyOnWriteArrayList<>();

// Поток 1: итерирует
for (String subscriber : subscribers) {
    notifyUser(subscriber);  // Может быть долго
}

// Поток 2: добавляет новых
subscribers.add("newUser");  // ✅ Не нарушит итерацию Потока 1

Заключение

  • Fail-fast — стандарт для обычных коллекций (ArrayList, HashMap), выбрасывает исключение при модификации
  • Fail-safe — для многопоточных сценариев (CopyOnWriteArrayList), работает с копией
  • Используй removeIf(), iterator.remove(), streams для безопасного удаления из fail-fast коллекций
  • Выбирай fail-safe коллекции только если действительно нужна потокобезопасность и высокая контенция читаемых операций