0Pricing
Java Academy · Урок

Отказоустойчивые и безопасные итераторы

Поймите ConcurrentModificationException и поведение итераторов с немедленным обнаружением изменений, а также узнайте, когда использовать итераторы по снимку.

«Отказоустойчивые и безопасные итераторы» — бесплатный урок Java Academy на CoddyKit. Это урок 4 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Java Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Java Academy содержит 4 уроков всего.

Итераторы с немедленным отказом и безопасные итераторы

Итераторы Java классифицируются по поведению при изменении базовой коллекции во время итерации: итераторы с немедленным отказом сразу выбрасывают исключение, а безопасные итераторы продолжают работу со снимком.

Итераторы с немедленным отказом

Большинство стандартных коллекций Java (ArrayList, HashMap, TreeSet) используют итераторы с немедленным отказом. Они отслеживают modCount — счётчик изменений. Любое структурное изменение во время итерации вызывает ConcurrentModificationException.

import java.util.*;

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

try {
    for (String s : list) {
        list.add("x"); // structural modification — throws!
    }
} catch (ConcurrentModificationException e) {
    System.out.println("ConcurrentModificationException caught!");
}
// Same for HashMap, TreeMap, HashSet, etc.

Зачем нужны итераторы с немедленным отказом

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

// modCount is incremented on every structural change
// (add, remove, clear on ArrayList)
// Iterator records modCount on creation
// On each next(), Iterator checks: if current modCount != expected, throw CME

// This detects bugs like:
List<Integer> nums = new ArrayList<>(List.of(1,2,3,4,5));
for (Integer n : nums) {
    if (n == 3) nums.remove(n); // bug caught immediately
}
// ConcurrentModificationException — not silent wrong results

Безопасное удаление: Iterator.remove()

Единственное безопасное изменение во время итерации на основе Iterator — это Iterator.remove(): он удаляет последний возвращённый элемент и обновляет modCount.

List<Integer> nums = new ArrayList<>(List.of(1,2,3,4,5,6));
Iterator<Integer> it = nums.iterator();

while (it.hasNext()) {
    int n = it.next();
    if (n % 2 == 0) it.remove(); // safe: updates modCount
}

System.out.println(nums); // [1, 3, 5]

removeIf: современная альтернатива

В Java 8 в Collection появился метод removeIf(Predicate) — более чистый способ удалять элементы, соответствующие условию, без ручного управления итератором.

List<String> names = new ArrayList<>(List.of("Alice", "Bob", "Ann", "Charlie"));

// Modern: removeIf handles iteration internally
names.removeIf(name -> name.startsWith("A"));
System.out.println(names); // [Bob, Charlie]

// Equivalent but verbose iterator approach:
Iterator<String> it = names.iterator();
while (it.hasNext()) {
    if (it.next().startsWith("A")) it.remove();
}

Безопасные итераторы: CopyOnWriteArrayList

CopyOnWriteArrayList использует безопасный итератор. Он обходит снимок, созданный в момент создания итератора, поэтому изменения не влияют на текущую итерацию.

import java.util.concurrent.CopyOnWriteArrayList;

CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.addAll(List.of("a", "b", "c"));

for (String s : list) {
    list.add("x"); // no ConcurrentModificationException!
    System.out.print(s + " "); // prints a, b, c (snapshot)
}
System.out.println();
System.out.println(list); // [a, b, c, x, x, x] — modified copy

Итератор ConcurrentHashMap

ConcurrentHashMap использует слабосогласованные итераторы: они отражают состояние в некоторый момент во время или после создания итератора. Исключение CME не выбрасывается, но итератор может увидеть или не увидеть одновременные изменения.

import java.util.concurrent.ConcurrentHashMap;

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

for (Map.Entry<String, Integer> e : map.entrySet()) {
    map.put("d", 4); // no CME — weakly consistent
    System.out.println(e.getKey() + "=" + e.getValue());
}
// May or may not print "d=4" — weakly consistent

Collections.synchronizedList

Collections.synchronizedList — это NOT безопасный вариант: он по-прежнему использует итераторы с немедленным отказом, но блокирует одновременный доступ. Во время итерации необходимо вручную выполнять синхронизацию.

import java.util.*;

List<String> syncList = Collections.synchronizedList(new ArrayList<>());
syncList.addAll(List.of("a", "b", "c"));

// Must synchronize during iteration!
synchronized (syncList) {
    for (String s : syncList) {
        System.out.println(s);
    }
}
// Without the synchronized block, CME is still possible from another thread

Шаблон итератора по снимку

Создайте снимок вручную, чтобы безопасно обойти изменяемую коллекцию без синхронизации.

import java.util.*;

List<String> original = new ArrayList<>(List.of("a", "b", "c"));

// Take a snapshot copy before iterating
List<String> snapshot = List.copyOf(original);

for (String s : snapshot) {
    // Safe to modify original during snapshot iteration
    original.remove(s);
    System.out.println("Removed: " + s);
}
System.out.println(original); // []

Компромиссы CopyOnWrite

CopyOnWriteArrayList безопасен, но дорог при записи. Каждое изменение создаёт полную копию.

  • Подходит для: множества операций чтения и редких операций записи (списки слушателей событий)
  • Не подходит для: частых изменений и больших коллекций

Итоги: итераторы с немедленным отказом и безопасные итераторы

Краткое сравнение:

  • Итераторы с немедленным отказом: ArrayList, HashMap, TreeMap — выбрасывают CME при изменении, проверяя modCount
  • Безопасные/слабосогласованные: ConcurrentHashMap, CopyOnWriteArrayList — не выбрасывают CME, обходят снимок или одновременные данные
  • Безопасные методы изменения: Iterator.remove(), removeIf(), replaceAll()

Практика: очистка просроченных сеансов

Безопасное удаление просроченных сеансов из одновременного списка с использованием подходящих шаблонов.

import java.util.*;
import java.util.concurrent.*;

class SessionManager {
    private final CopyOnWriteArrayList<Session> sessions = new CopyOnWriteArrayList<>();

    record Session(String id, long expiresAt) {
        boolean isExpired() { return System.currentTimeMillis() > expiresAt; }
    }

    void add(Session s) { sessions.add(s); }

    // Safe to call from multiple threads while iterating
    void purgeExpired() {
        sessions.removeIf(Session::isExpired);
    }

    List<Session> active() {
        return sessions.stream().filter(s -> !s.isExpired()).toList();
    }
}

Быстрая проверка

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

Итоги: итераторы с немедленным отказом и безопасные итераторы

Главные выводы:

  • Итераторы с немедленным отказом (ArrayList, HashMap) выбрасывают ConcurrentModificationException при одновременном изменении
  • Безопасные итераторы (CopyOnWriteArrayList, ConcurrentHashMap) позволяют изменять коллекцию без исключения
  • Iterator.remove() — единственный безопасный способ удаления во время итерации с немедленным отказом
  • removeIf() — современная и более чистая альтернатива ручному вызову Iterator.remove()
  • CopyOnWriteArrayList подходит для сценариев с частым чтением и редкой записью
  • ConcurrentHashMap является слабосогласованным: CME не возникает, а одновременные изменения могут быть видны или не видны

Часто задаваемые вопросы

Урок «Отказоустойчивые и безопасные итераторы» бесплатный?

Да — полный текст урока «Отказоустойчивые и безопасные итераторы» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Java Academy, подпишись на CoddyKit PRO. Курс Java Academy содержит 4 уроков всего.

Чему я научусь в уроке «Отказоустойчивые и безопасные итераторы»?

Поймите ConcurrentModificationException и поведение итераторов с немедленным обнаружением изменений, а также узнайте, когда использовать итераторы по снимку. Ты практикуешь Java Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать Java Academy?

Предыдущий опыт не требуется. Java Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 4 из 4.

Сколько времени занимает урок «Отказоустойчивые и безопасные итераторы»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке Java Academy?

Да. Каждый урок Java Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Контракты Iterable и Iterator
  2. Реализация собственного итератора
  3. ListIterator и двунаправленный обход
  4. Отказоустойчивые и безопасные итераторы
← Назад к Java Academy