Итератор (Iterator)
Итератор выдаёт элементы, не раскрывая устройство структуры. Алгоритм обхода и позиция хранятся в отдельном объекте.
Сигналы применения
- клиентам нужен обход, но им нельзя отдавать внутренний массив, дерево или граф;
- для одной структуры требуется несколько способов обхода;
- обход должен быть ленивым;
- несколько клиентов должны перемещаться по одной коллекции независимо.
В Java основной контракт — Iterator<T>, а источник итераторов — Iterable<T>.
Роли и протокол
- Aggregate предоставляет новый Iterator;
- Iterator отвечает на
hasNext()иnext(); - Concrete Iterator хранит очередь, стек, индекс или другой курсор;
- Client потребляет элементы, не управляя внутренней структурой.
После исчерпания next() бросает NoSuchElementException: null может быть элементом.
Пример: обход графа в ширину
final class IntGraph implements Iterable<Integer> {
private final Map<Integer, List<Integer>> edges;
private final int start;
IntGraph(Map<Integer, List<Integer>> edges, int start) {
this.edges = edges; this.start = start;
}
@Override public Iterator<Integer> iterator() {
return new BfsIterator(edges, start); }
}
final class BfsIterator implements Iterator<Integer> {
private final Map<Integer, List<Integer>> edges;
private final Queue<Integer> queue = new ArrayDeque<>();
private final Set<Integer> visited = new HashSet<>();
BfsIterator(Map<Integer, List<Integer>> edges, int start) {
this.edges = edges; queue.add(start); visited.add(start);
}
public boolean hasNext() { return !queue.isEmpty(); }
public Integer next() {
if (!hasNext()) throw new NoSuchElementException();
int current = queue.remove();
for (int neighbor : edges.getOrDefault(current, List.of())) {
if (visited.add(neighbor)) queue.add(neighbor);
}
return current;
}
}Каждый iterator() создаёт свои queue и visited, а циклы не зацикливают обход.
Изменение коллекции во время обхода
Итераторы стандартных изменяемых коллекций часто fail-fast: при структурном изменении
не через итератор они могут выбросить ConcurrentModificationException. Это
диагностика, а не гарантия потокобезопасности.
Для снимка итерируются по копии. Конкурентный обход может быть weakly-consistent без гарантии единого снимка.
Компромиссы
- скрывается устройство коллекции;
- ленивый обход экономит память;
- состояние обхода требует отдельного объекта;
- сложный Iterator труднее отлаживать, чем готовый список;
- бесконечный итератор требует ограничивающей операции со стороны клиента.
Чем отличается
Stream— вычислительный конвейер, Iterator — протокол следующего элемента;- Цепочка обязанностей Chain of Responsibility передаёт запрос, Iterator обходит;
- Composite часто предоставляет Iterator для обхода своего дерева.
Вопросы на собеседовании
- Почему контракты разделены? Источник создаёт независимые курсоры.
- Что делает
for-each? Получаетiterator()и вызываетhasNext()/next(). - Обязателен ли
remove()? Нет; по умолчанию он не поддерживается. - Fail-fast означает потокобезопасность? Нет.
- Как обойти большой набор? Лениво; внешний ресурс при этом нужно закрывать. Связи: Поведенческие паттерны, Iterable, Коллекции.