Отказоустойчивые и безопасные итераторы
Поймите 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 consistentCollections.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 — локальная установка не требуется.
Все уроки этого курса
- Контракты Iterable и Iterator
- Реализация собственного итератора
- ListIterator и двунаправленный обход
- Отказоустойчивые и безопасные итераторы