Что такое Iterator pattern
В Java паттерн встроен на уровне ядра через два ключевых интерфейса:
🟢 Junior Level
Iterator (Итератор) — это поведенческий паттерн проектирования, который предоставляет стандартизированный способ последовательного обхода элементов составного объекта (коллекции), не раскрывая при этом его внутреннего представления и структуры данных (массив, связный список, дерево или граф).
В Java паттерн встроен на уровне ядра через два ключевых интерфейса:
Iterable<T>: интерфейс коллекции, объявляющий методIterator<T> iterator(). Любой класс, реализующийIterable, можно использовать в циклеfor-each.Iterator<T>: сам итератор с методамиhasNext(),next(),default void remove().
Бытовая аналогия
- Плеер с музыкальным плейлистом: Нажимая кнопку «Next», слушатель переходит к следующему треку, не задумываясь, как устроен плейлист — это локальные MP3-файлы на диске, потоковый сетевой буфер или облачная база данных. Плеер хранит текущую позицию воспроизведения и знает, есть ли следующий трек.
- Турникет в метро: Пассажиры подходят по одному; турникет пропускает текущего человека и ждет следующего, не требуя видеть всю толпу на площади одновременно.
Базовый пример в Java
import java.util.List;
import java.util.Iterator;
public class IteratorExample {
public static void main(String[] args) {
List<String> frameworks = List.of("Spring", "Hibernate", "Quarkus");
// 1. Явное использование Iterator
Iterator<String> iterator = frameworks.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
System.out.println("Фреймворк: " + item);
}
// 2. Цикл for-each (синтаксический сахар над Iterator)
for (String item : frameworks) {
System.out.println("For-each: " + item);
}
}
}
🟡 Middle Level
Iterator vs ListIterator
Интерфейс ListIterator<E> расширяет Iterator<E> и доступен только для коллекций, реализующих интерфейс List (списки с прямым доступом по индексу):
| Возможность | Iterator<E> |
ListIterator<E> |
|---|---|---|
| Направление обхода | Только вперед (next()) |
Двунаправленный (next() и previous()) |
| Проверка границ | hasNext() |
hasNext() и hasPrevious() |
| Получение индекса | Не поддерживается | nextIndex(), previousIndex() |
| Модификация данных | Только удаление remove() |
Удаление remove(), замена set(E), добавление add(E) |
| Применимость | Любая Collection (Set, Queue, List) |
Только List (ArrayList, LinkedList) |
List<String> list = new ArrayList<>(List.of("A", "B", "C"));
ListIterator<String> listIterator = list.listIterator();
while (listIterator.hasNext()) {
String val = listIterator.next();
if ("B".equals(val)) {
listIterator.set("B_MODIFIED"); // Замена текущего элемента
listIterator.add("INSERTED"); // Вставка перед следующим
}
}
Как работает for-each под капотом
Цикл for (Element e : collection) компилируется байткод-компилятором javac в стандартный вызов итератора.
// Исходный код:
for (String item : list) {
System.out.println(item);
}
// Эквивалентный байткод / декомпилированный код:
for (Iterator<String> it = list.iterator(); it.hasNext(); ) {
String item = it.next();
System.out.println(item);
}
Именно поэтому попытка изменить размер коллекции внутри цикла for-each (list.add(...) или list.remove(...)) приводит к ConcurrentModificationException: модифицируется коллекция, а не итератор!
Реализация собственного Iterable & Iterator
Пример реализации итератора для обхода бинарного дерева поиска в порядке возрастания (In-Order Traversal):
import java.util.*;
public class BinaryTree<T> implements Iterable<T> {
private Node<T> root;
private static class Node<T> {
T value;
Node<T> left;
Node<T> right;
Node(T val) { this.value = val; }
}
@Override
public Iterator<T> iterator() {
return new InOrderIterator(root);
}
private class InOrderIterator implements Iterator<T> {
private final Deque<Node<T>> stack = new ArrayDeque<>();
public InOrderIterator(Node<T> root) {
pushLeftBranch(root);
}
private void pushLeftBranch(Node<T> node) {
while (node != null) {
stack.push(node);
node = node.left;
}
}
@Override
public boolean hasNext() {
return !stack.isEmpty();
}
@Override
public T next() {
if (!hasNext()) throw new NoSuchElementException();
Node<T> current = stack.pop();
pushLeftBranch(current.right);
return current.value;
}
}
}
🔴 Senior Level
Механизм Fail-Fast: modCount и expectedModCount
Стандартные коллекции (ArrayList, HashMap, HashSet, LinkedList) реализуют поведение Fail-Fast:
- Коллекция содержит внутреннее поле
protected transient int modCount = 0;. - Каждая структурная операция (
add,remove,clear,ensureCapacity) инкрементируетmodCount++. - При создании итератора запоминается текущее значение счетчика:
int expectedModCount = modCount;. - При каждом вызове
next()илиremove()итератор выполняет проверку:final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }
⚠️ Важно для Senior: Спецификация JDK подчеркивает: Fail-Fast поведение не гарантируется на 100% в многопоточной среде без внешней синхронизации.
modCountне являетсяvolatile-переменной, поэтому изменения одного потока могут быть не видны другому потоку немедленно из-за кэшей процессора. Fail-Fast предназначен исключительно для выявления багов при разработке.
Fail-Fast vs Fail-Safe vs Weakly Consistent
| Категория | Представители | Поведение при модификации | Читает ли новые данные? | remove() итератора |
|---|---|---|---|---|
| Fail-Fast | ArrayList, HashMap, LinkedList |
Немедленно бросает ConcurrentModificationException |
Нет, обход прерывается | Поддерживается (синхронизирует modCount) |
| Fail-Safe (Snapshot) | CopyOnWriteArrayList, CopyOnWriteArraySet |
Не бросает исключений, итерирует по неизменяемому снимку массива | Не видит изменений, сделанных после старта итератора | UnsupportedOperationException |
| Weakly Consistent | ConcurrentHashMap, ConcurrentLinkedQueue |
Не бросает исключений, гарантирует обход без падений | Может увидеть некоторые изменения, сделанные параллельно | Поддерживается |
Spliterator в Java 8: Эволюция под Stream API и Fork/Join
С появлением Java 8 и параллельных стримов (Parallel Streams) возможностей Iterator оказалось недостаточно: обычный итератор нельзя эффективно распараллелить на несколько ядер CPU. Для этого был спроектирован Spliterator<T> (Splitable Iterator):
public interface Spliterator<T> {
boolean tryAdvance(Consumer<? super T> action); // Аналог hasNext() + next()
Spliterator<T> trySplit(); // Разделяет диапазон данных пополам!
long estimateSize(); // Примерное количество оставшихся элементов
int characteristics(); // Битовые флаги свойств источника
}
trySplit(): Ключевой метод для работы с пуломForkJoinPool.commonPool(). Источник данных рекурсивно делится на две равные части: одна остается у текущего потока, вторая передается соседнему потоку в пуле.characteristics(): Битовая маска характеристик для оптимизации конвейера стрима:ORDERED(важен порядок),DISTINCT(элементы уникальны),SORTED(коллекция отсортирована),SIZED(точный размер известен),NONNULL(нетnull),IMMUTABLE,CONCURRENT. Например, если стрим видит флагDISTINCT, операция.distinct()исключается из графа выполнения как избыточная.
Примитивные итераторы и GC-overhead
Использование Iterator<Integer> в критических по производительности участках приводит к постоянному автобоксингу (int $\rightarrow$ Integer), создавая миллионы короткоживущих объектов в Eden Space и нагружая сборщик мусора (GC).
В Java для предотвращения боксинга используются специализированные примитивные итераторы:
PrimitiveIterator.OfIntPrimitiveIterator.OfLongPrimitiveIterator.OfDouble
int[] array = {1, 2, 3, 4, 5};
PrimitiveIterator.OfInt it = Arrays.stream(array).iterator();
while (it.hasNext()) {
int val = it.nextInt(); // 0 аллокаций, отсутствие boxing!
}
🎯 Шпаргалка для интервью
30-секундный ответ (Elevator Pitch)
«Iterator — это поведенческий паттерн, обеспечивающий универсальный последовательный доступ к элементам коллекции без раскрытия её внутреннего устройства. В Java он представлен интерфейсами
IterableиIterator, на которых основан циклfor-each. Основные подвиды в Java: Fail-Fast (стандартные коллекции, отслеживают структурные изменения черезmodCountи выбрасываютConcurrentModificationException), Fail-Safe (по снимку вCopyOnWriteArrayList), и Weakly Consistent (вConcurrentHashMap). Для многопоточного параллельного выполнения в Stream API используется Spliterator, поддерживающий деление диапазона данных методомtrySplit()».
3–4 каверзных вопроса с ответами
- Как корректно удалить элементы из коллекции во время обхода?
- Ответ:
- Через
iterator.remove()в классическом циклеwhile (it.hasNext()). - Начиная с Java 8 — с помощью декларативного метода
collection.removeIf(predicate), который внутри оптимизирован под конкретную структуру данных (например, вArrayListсдвиг элементов происходит за один проход $O(N)$ вместо $O(N^2)$ при повторныхremove()).
- Через
- Ответ:
- Почему
modCountне гарантирует выбросConcurrentModificationExceptionв многопоточном коде?- Ответ: Поле
modCountне объявлено какvolatile. В многопоточной среде значение переменной может оседать в регистрах процессора или кэшах L1/L2 ядра. Без синхронизации один поток может изменить коллекцию, но второй поток в методеcheckForComodification()прочитает устаревшее значениеexpectedModCount == modCountи продолжит работу, что может привести к порче данных вместо исключения.
- Ответ: Поле
- В чём разница между
SpliteratorиIterator?- Ответ:
Iteratorрассчитан строго на последовательный обход одним потоком (hasNext()/next()).Spliteratorспроектирован для параллельной обработки: он умеет делить данные пополам с помощьюtrySplit(), сообщает размерestimateSize()и объявляет характеристики (SORTED,DISTINCT,SIZED), позволяющиеStream APIотключать лишние фазы вычислений.
- Ответ:
- Почему вызов
remove()у итератораCopyOnWriteArrayListприводит к исключению?- Ответ: Итератор в
CopyOnWriteArrayListработает по неизменяемому статическому снимку базового массива (final Object[] snapshot), зафиксированному в момент создания итератора. Поскольку сам снимок иммутабелен, методremove()не поддерживается и выбрасываетUnsupportedOperationException.
- Ответ: Итератор в
Типичные ошибки и Red Flags
- 🚩 Попытка модифицировать коллекцию (
list.remove(...)/list.add(...)) внутри циклаfor-each(гарантированныйConcurrentModificationException). - 🚩 Утверждение, что
ConcurrentModificationExceptionвозникает только при многопоточном доступе (чаще всего он возникает в одном потоке из-за неправильного удаления в цикле). - 🚩 Заблуждение, что Fail-Safe итератор видит абсолютно свежие данные (он видит снимок, сделанный в момент его создания).
- 🚩 Использование
Iterator<Integer>вместоPrimitiveIterator.OfIntв высоконагруженных циклах обработки примитивов.
Связанные темы
- Какие основные интерфейсы Collection — иерархия коллекций и метод
iterator() - Что такое CopyOnWriteArrayList — устройство snapshot-итераторов
- Что такое Stream API — внутренняя работа со
Spliterator - Какие категории паттернов существуют — поведенческие шаблоны GoF
- Что такое паттерны проектирования — основы паттернов проектирования