🧩 Раздел 2 · Вопрос #15

Что такое 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:

  1. Коллекция содержит внутреннее поле protected transient int modCount = 0;.
  2. Каждая структурная операция (add, remove, clear, ensureCapacity) инкрементирует modCount++.
  3. При создании итератора запоминается текущее значение счетчика: int expectedModCount = modCount;.
  4. При каждом вызове 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();                           // Битовые флаги свойств источника
}
  1. trySplit(): Ключевой метод для работы с пулом ForkJoinPool.commonPool(). Источник данных рекурсивно делится на две равные части: одна остается у текущего потока, вторая передается соседнему потоку в пуле.
  2. characteristics(): Битовая маска характеристик для оптимизации конвейера стрима:
    • ORDERED (важен порядок), DISTINCT (элементы уникальны), SORTED (коллекция отсортирована),
    • SIZED (точный размер известен), NONNULL (нет null), IMMUTABLE, CONCURRENT. Например, если стрим видит флаг DISTINCT, операция .distinct() исключается из графа выполнения как избыточная.

Примитивные итераторы и GC-overhead

Использование Iterator<Integer> в критических по производительности участках приводит к постоянному автобоксингу (int $\rightarrow$ Integer), создавая миллионы короткоживущих объектов в Eden Space и нагружая сборщик мусора (GC).

В Java для предотвращения боксинга используются специализированные примитивные итераторы:

  • PrimitiveIterator.OfInt
  • PrimitiveIterator.OfLong
  • PrimitiveIterator.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 каверзных вопроса с ответами

  1. Как корректно удалить элементы из коллекции во время обхода?
    • Ответ:
      1. Через iterator.remove() в классическом цикле while (it.hasNext()).
      2. Начиная с Java 8 — с помощью декларативного метода collection.removeIf(predicate), который внутри оптимизирован под конкретную структуру данных (например, в ArrayList сдвиг элементов происходит за один проход $O(N)$ вместо $O(N^2)$ при повторных remove()).
  2. Почему modCount не гарантирует выброс ConcurrentModificationException в многопоточном коде?
    • Ответ: Поле modCount не объявлено как volatile. В многопоточной среде значение переменной может оседать в регистрах процессора или кэшах L1/L2 ядра. Без синхронизации один поток может изменить коллекцию, но второй поток в методе checkForComodification() прочитает устаревшее значение expectedModCount == modCount и продолжит работу, что может привести к порче данных вместо исключения.
  3. В чём разница между Spliterator и Iterator?
    • Ответ: Iterator рассчитан строго на последовательный обход одним потоком (hasNext()/next()). Spliterator спроектирован для параллельной обработки: он умеет делить данные пополам с помощью trySplit(), сообщает размер estimateSize() и объявляет характеристики (SORTED, DISTINCT, SIZED), позволяющие Stream API отключать лишние фазы вычислений.
  4. Почему вызов remove() у итератора CopyOnWriteArrayList приводит к исключению?
    • Ответ: Итератор в CopyOnWriteArrayList работает по неизменяемому статическому снимку базового массива (final Object[] snapshot), зафиксированному в момент создания итератора. Поскольку сам снимок иммутабелен, метод remove() не поддерживается и выбрасывает UnsupportedOperationException.

Типичные ошибки и Red Flags

  • 🚩 Попытка модифицировать коллекцию (list.remove(...) / list.add(...)) внутри цикла for-each (гарантированный ConcurrentModificationException).
  • 🚩 Утверждение, что ConcurrentModificationException возникает только при многопоточном доступе (чаще всего он возникает в одном потоке из-за неправильного удаления в цикле).
  • 🚩 Заблуждение, что Fail-Safe итератор видит абсолютно свежие данные (он видит снимок, сделанный в момент его создания).
  • 🚩 Использование Iterator<Integer> вместо PrimitiveIterator.OfInt в высоконагруженных циклах обработки примитивов.

Связанные темы