☕ Java

Итераторы и генераторы в Java: подробное руководство

Что это такое и зачем? (аналогия из реальной жизни)

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

  1. Способ «жадный»: Вы снимаете с полки все книги, несёте их к столу, перебираете, выписываете нужные, а потом несёте обратно. Если книг 10 000, то стол будет завален, вы устанете таскать, а если книг миллион — стол просто сломается.
  2. Способ «ленивый»: Вы берёте блокнот и ручку, подходите к стеллажу, смотрите на первую книгу. Если она подходит — записываете. Переходите ко второй. И так далее. Вы не таскаете книги, вы просто перебираете их по одной.

В программировании итератор — это и есть тот самый «ленивый библиотекарь». Это специальный объект, который умеет перебирать элементы коллекции (списка, множества, массива) по одному, не загружая всю коллекцию в оперативную память целиком.

А генератор — это ещё более крутая штука. Это как если бы вы не просто перебирали готовые книги, а сами писали их названия на ходу. Например, вы генерируете бесконечный список чисел Фибоначчи. Вы не храните их все в памяти — вы выдаёте следующее число только тогда, когда его просят.

Важно: В Java нет встроенного ключевого слова yield, как в Python или C#. Генераторы в Java реализуются через интерфейсы Iterator и Iterable, но с хитрой логикой «ленивых» вычислений.

Зачем это нужно?

  • Экономия памяти: Вы можете обработать файл размером 100 ГБ, читая его по одной строке, а не загружая целиком.
  • Единый интерфейс: Неважно, что у вас внутри — массив, список, дерево или поток из сети. Итератор даёт единый способ перебора: «дай следующий элемент».
  • Ленивые вычисления: Вы можете создать бесконечную последовательность (например, все простые числа) и брать из неё ровно столько, сколько нужно.

Теория: как это работает изнутри

Давайте заглянем «под капот» Java и разберёмся, как устроены итераторы на уровне интерфейсов.

Интерфейс Iterator (дженерик)

Это главный «двигатель» перебора. Он находится в пакете java.util и содержит три метода:

Метод Описание
boolean hasNext() Возвращает true, если в коллекции есть ещё элементы для перебора. Если элементов больше нет — false.
T next() Возвращает следующий элемент коллекции. Если элементов больше нет — бросает исключение NoSuchElementException.
void remove() Удаляет последний возвращённый элемент из коллекции. Используется редко, так как может привести к ошибкам.

Как это работает в цикле?

Когда вы пишете for (String s : list), компилятор Java невидимо для вас превращает этот код в следующее:

// То, что вы пишете:
for (String s : list) {
    System.out.println(s);
}

// То, во что это превращается (упрощённо):
Iterator<String> it = list.iterator();  // Получаем итератор
while (it.hasNext()) {                  // Пока есть следующий элемент
    String s = it.next();               // Берём его
    System.out.println(s);              // Делаем что-то
}

Интерфейс Iterable

Чтобы объект можно было использовать в цикле for-each, он должен реализовать интерфейс Iterable. У него всего один метод:

public interface Iterable<T> {
    Iterator<T> iterator();  // Возвращает итератор для этого объекта
}

Все стандартные коллекции Java (ArrayList, HashSet, LinkedList и т.д.) реализуют этот интерфейс. Поэтому их можно перебирать в цикле.

Что такое «внутреннее состояние» итератора?

Каждый итератор хранит в себе текущую позицию в коллекции. Например, для ArrayList это просто число — индекс текущего элемента. Когда вы вызываете next(), индекс увеличивается на 1. Когда индекс становится равен размеру списка — hasNext() возвращает false.

Для более сложных структур (например, бинарное дерево) итератор может хранить целый стек узлов, чтобы «запоминать», куда идти дальше.

Важное замечание: Итератор — это одноразовый объект. Вы не можете «перемотать» его назад. Если нужно пройтись по коллекции заново — нужно запросить новый итератор через collection.iterator().

Генераторы: ленивая последовательность

В Java нет ключевого слова yield, но мы можем создать свой собственный «генератор» — класс, который реализует Iterator и вычисляет следующий элемент только в момент вызова next().

Представьте себе конвейер: на входе — сырьё (данные), на выходе — готовые детали. Генератор — это станок, который делает одну деталь за раз, только когда её заказывают. Он не делает все детали заранее и не складирует их.

Синтаксис в Java: все формы и варианты

Давайте рассмотрим все способы работы с итераторами в Java — от классики до современных нововведений.

1. Классический способ (Java 1.2+)

Самый старый и явный способ. Вы вручную получаете итератор и вызываете его методы.

import java.util.*;

public class ClassicIteratorExample {
    public static void main(String[] args) {
        List<String> fruits = new ArrayList<>();
        fruits.add("Яблоко");
        fruits.add("Груша");
        fruits.add("Апельсин");

        // Получаем итератор
        Iterator<String> iterator = fruits.iterator();

        // Перебираем элементы
        while (iterator.hasNext()) {
            String fruit = iterator.next();
            System.out.println("Фрукт: " + fruit);
        }
    }
}

2. Цикл for-each (Java 5+)

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

import java.util.*;

public class ForEachExample {
    public static void main(String[] args) {
        Set<Integer> numbers = new HashSet<>();
        numbers.add(10);
        numbers.add(20);
        numbers.add(30);

        // Простой и элегантный перебор
        for (Integer num : numbers) {
            System.out.println("Число: " + num);
        }
    }
}

3. Удаление элементов во время перебора

Если вы попытаетесь удалить элемент из коллекции во время цикла for-each, вы получите ConcurrentModificationException. Для безопасного удаления нужно использовать итератор явно.

import java.util.*;

public class SafeRemoveExample {
    public static void main(String[] args) {
        List<String> names = new ArrayList<>();
        names.add("Анна");
        names.add("Иван");
        names.add("Мария");
        names.add("Пётр");

        // Удаляем все имена, начинающиеся на 'А'
        Iterator<String> iterator = names.iterator();
        while (iterator.hasNext()) {
            String name = iterator.next();
            if (name.startsWith("А")) {
                iterator.remove();  // Безопасное удаление!
            }
        }

        System.out.println("Остались: " + names); // [Иван, Мария, Пётр]
    }
}

4. ListIterator — двунаправленный итератор

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

import java.util.*;

public class ListIteratorExample {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("A");
        list.add("B");
        list.add("C");

        ListIterator<String> listIterator = list.listIterator();

        // Идём вперёд
        System.out.println("Вперёд:");
        while (listIterator.hasNext()) {
            System.out.println(listIterator.next());
        }

        // Идём назад
        System.out.println("Назад:");
        while (listIterator.hasPrevious()) {
            System.out.println(listIterator.previous());
        }
    }
}

5. Создание своего итератора (генератор)

Реализуем интерфейс Iterator и создаём «ленивый» генератор чисел Фибоначчи.

import java.util.Iterator;

// Генератор чисел Фибоначчи
class FibonacciGenerator implements Iterator<Integer> {
    private int prev = 0;  // Предыдущее число
    private int curr = 1;  // Текущее число
    private int limit;     // Сколько чисел выдать

    public FibonacciGenerator(int limit) {
        this.limit = limit;
    }

    @Override
    public boolean hasNext() {
        return limit > 0;  // Пока не выдали все числа
    }

    @Override
    public Integer next() {
        if (!hasNext()) {
            throw new java.util.NoSuchElementException();
        }
        int result = prev;          // Берём текущее значение
        int next = prev + curr;     // Вычисляем следующее
        prev = curr;                // Сдвигаем
        curr = next;                // Сдвигаем
        limit--;                    // Уменьшаем счётчик
        return result;              // Возвращаем результат
    }
}

public class CustomIteratorExample {
    public static void main(String[] args) {
        FibonacciGenerator fib = new FibonacciGenerator(10);
        System.out.println("Первые 10 чисел Фибоначчи:");
        while (fib.hasNext()) {
            System.out.print(fib.next() + " ");
        }
        // Вывод: 0 1 1 2 3 5 8 13 21 34
    }
}

6. Анонимный класс итератора

Можно создать итератор «на лету», не объявляя отдельный класс.

import java.util.Iterator;

public class AnonymousIteratorExample {
    public static void main(String[] args) {
        // Создаём итератор, который выдаёт степени двойки
        Iterator<Integer> powersOfTwo = new Iterator<>() {
            private int current = 1;

            @Override
            public boolean hasNext() {
                return current < 1000;  // Ограничимся 1000
            }

            @Override
            public Integer next() {
                int result = current;
                current *= 2;
                return result;
            }
        };

        System.out.println("Степени двойки до 1000:");
        while (powersOfTwo.hasNext()) {
            System.out.print(powersOfTwo.next() + " ");
        }
        // Вывод: 1 2 4 8 16 32 64 128 256 512
    }
}

7. Stream API и Spliterator (Java 8+)

Современный способ перебора — через стримы. Внутри они используют Spliterator — улучшенную версию итератора, которая поддерживает параллельную обработку.

import java.util.*;
import java.util.stream.*;

public class StreamIteratorExample {
    public static void main(String[] args) {
        List<String> cities = Arrays.asList("Москва", "Париж", "Лондон", "Токио");

        // Перебор через стрим
        cities.stream()
              .filter(city -> city.length() > 5)  // Оставляем только длинные названия
              .map(String::toUpperCase)           // Переводим в верхний регистр
              .forEach(System.out::println);      // Печатаем

        // Вывод:
        // МОСКВА
        // ЛОНДОН
    }
}

Примеры кода — от простого к сложному

Пример 1: Перебор массива через итератор (адаптер)

Массивы не реализуют Iterable, но мы можем сделать адаптер.

import java.util.Iterator;

public class ArrayIteratorExample {
    public static void main(String[] args) {
        String[] colors = {"Красный", "Зелёный", "Синий"};

        // Превращаем массив в Iterable
        Iterable<String> iterableColors = () -> new Iterator<>() {
            private int index = 0;

            @Override
            public boolean hasNext() {
                return index < colors.length;
            }

            @Override
            public String next() {
                return colors[index++];
            }
        };

        // Теперь можно использовать for-each
        for (String color : iterableColors) {
            System.out.println("Цвет: " + color);
        }
    }
}

Пример 2: Бесконечный генератор случайных чисел

import java.util.Iterator;
import java.util.Random;

class RandomGenerator implements Iterator<Integer> {
    private Random random = new Random();

    @Override
    public boolean hasNext() {
        return true;  // Всегда есть следующий элемент!
    }

    @Override
    public Integer next() {
        return random.nextInt(100);  // Случайное число от 0 до 99
    }
}

public class InfiniteRandomExample {
    public static void main(String[] args) {
        RandomGenerator gen = new RandomGenerator();
        System.out.println("10 случайных чисел:");
        for (int i = 0; i < 10; i++) {
            System.out.print(gen.next() + " ");
        }
        // Вывод: что-то вроде 42 17 89 3 56 ...
    }
}

Пример 3: Генератор простых чисел

import java.util.Iterator;

class PrimeGenerator implements Iterator<Integer> {
    private int current = 2;  // Первое простое число

    private boolean isPrime(int n) {
        if (n < 2) return false;
        for (int i = 2; i * i <= n; i++) {
            if (n % i == 0) return false;
        }
        return true;
    }

    @Override
    public boolean hasNext() {
        return true;  // Простых чисел бесконечно много
    }

    @Override
    public Integer next() {
        while (!isPrime(current)) {
            current++;
        }
        return current++;  // Возвращаем и переходим к следующему
    }
}

public class PrimeExample {
    public static void main(String[] args) {
        PrimeGenerator primes = new PrimeGenerator();
        System.out.println("Первые 15 простых чисел:");
        for (int i = 0; i < 15; i++) {
            System.out.print(primes.next() + " ");
        }
        // Вывод: 2 3 5 7 11 13 17 19 23 29 31 37 41 43 47
    }
}

Пример 4: Итератор по строкам файла (ленивое чтение)

import java.io.*;
import java.util.Iterator;

class FileLineIterator implements Iterator<String> {
    private BufferedReader reader;
    private String nextLine;

    public FileLineIterator(String filename) throws IOException {
        reader = new BufferedReader(new FileReader(filename));
        nextLine = reader.readLine();  // Читаем первую строку
    }

    @Override
    public boolean hasNext() {
        return nextLine != null;
    }

    @Override
    public String next() {
        String current = nextLine;
        try {
            nextLine = reader.readLine();  // Читаем следующую строку
            if (nextLine == null) {
                reader.close();  // Закрываем файл, если строк больше нет
            }
        } catch (IOException e) {
            nextLine = null;
        }
        return current;
    }
}

public class FileReadExample {
    public static void main(String[] args) throws IOException {
        // Создаём временный файл для примера
        File tempFile = File.createTempFile("test", ".txt");
        tempFile.deleteOnExit();
        try (PrintWriter pw = new PrintWriter(tempFile)) {
            pw.println("Строка 1");
            pw.println("Строка 2");
            pw.println("Строка 3");
        }

        FileLineIterator fileIterator = new FileLineIterator(tempFile.getAbsolutePath());
        System.out.println("Содержимое файла:");
        while (fileIterator.hasNext()) {
            System.out.println(fileIterator.next());
        }
    }
}

Пример 5: Комбинированный итератор (объединение двух списков)

import java.util.*;

class CombinedIterator<T> implements Iterator<T> {
    private Iterator<T> first;
    private Iterator<T> second;

    public CombinedIterator(Iterator<T> first, Iterator<T> second) {
        this.first = first;
        this.second = second;
    }

    @Override
    public boolean hasNext() {
        return first.hasNext() || second.hasNext();
    }

    @Override
    public T next() {
        if (first.hasNext()) {
            return first.next();
        } else if (second.hasNext()) {
            return second.next();
        } else {
            throw new NoSuchElementException();
        }
    }
}

public class CombineExample {
    public static void main(String[] args) {
        List<String> list1 = Arrays.asList("A", "B", "C");
        List<String> list2 = Arrays.asList("X", "Y", "Z");

        CombinedIterator<String> combined = 
            new CombinedIterator<>(list1.iterator(), list2.iterator());

        System.out.println("Объединённый список:");
        while (combined.hasNext()) {
            System.out.print(combined.next() + " ");
        }
        // Вывод: A B C X Y Z
    }
}

Пример 6: Итератор с фильтрацией

import java.util.*;
import java.util.function.Predicate;

class FilteredIterator<T> implements Iterator<T> {
    private Iterator<T> source;
    private Predicate<T> predicate;
    private T nextElement;
    private boolean hasNextElement;

    public FilteredIterator(Iterator<T> source, Predicate<T> predicate) {
        this.source = source;
        this.predicate = predicate;
        advance();  // Находим первый подходящий элемент
    }

    private void advance() {
        hasNextElement = false;
        while (source.hasNext()) {
            T element = source.next();
            if (predicate.test(element)) {
                nextElement = element;
                hasNextElement = true;
                break;
            }
        }
    }

    @Override
    public boolean hasNext() {
        return hasNextElement;
    }

    @Override
    public T next() {
        if (!hasNextElement) {
            throw new NoSuchElementException();
        }
        T result = nextElement;
        advance();  // Ищем следующий подходящий элемент
        return result;
    }
}

public class FilterExample {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

        // Оставляем только чётные числа
        FilteredIterator<Integer> evenNumbers = 
            new FilteredIterator<>(numbers.iterator(), n -> n % 2 == 0);

        System.out.println("Чётные числа:");
        while (evenNumbers.hasNext()) {
            System.out.print(evenNumbers.next() + " ");
        }
        // Вывод: 2 4 6 8 10
    }
}

Пример 7: Итератор для дерева (обход в глубину)

import java.util.*;

class TreeNode<T> {
    T value;
    List<TreeNode<T>> children;

    public TreeNode(T value) {
        this.value = value;
        this.children = new ArrayList<>();
    }

    public void addChild(TreeNode<T> child) {
        children.add(child);
    }
}

class DepthFirstIterator<T> implements Iterator<T> {
    private Stack<TreeNode<T>> stack = new Stack<>();

    public DepthFirstIterator(TreeNode<T> root) {
        stack.push(root);
    }

    @Override
    public boolean hasNext() {
        return !stack.isEmpty();
    }

    @Override
    public T next() {
        if (!hasNext()) {
            throw new NoSuchElementException();
        }
        TreeNode<T> node = stack.pop();
        // Добавляем детей в обратном порядке, чтобы сохранить порядок обхода
        for (int i = node.children.size() - 1; i >= 0; i--) {
            stack.push(node.children.get(i));
        }
        return node.value;
    }
}

public class TreeIteratorExample {
    public static void main(String[] args) {
        // Строим дерево:
        //       A
        //     /   
        //    B     C
        //   /    / 
        //  D   E F   G
        TreeNode<String> root = new TreeNode<>("A");
        TreeNode<String> b = new TreeNode<>("B");
        TreeNode<String> c = new TreeNode<>("C");
        TreeNode<String> d = new TreeNode<>("D");
        TreeNode<String> e = new TreeNode<>("E");
        TreeNode<String> f = new TreeNode<>("F");
        TreeNode<String> g = new TreeNode<>("G");

        root.addChild(b);
        root.addChild(c);
        b.addChild(d);
        b.addChild(e);
        c.addChild(f);
        c.addChild(g);

        DepthFirstIterator<String> dfs = new DepthFirstIterator<>(root);
        System.out.println("Обход дерева в глубину:");
        while (dfs.hasNext()) {
            System.out.print(dfs.next() + " ");
        }
        // Вывод: A B D E C F G
    }
}

Сравнение с Python: чем похоже и чем отличается

Характеристика Java Python
Ключевое слово для генераторов Нет встроенного. Используется интерфейс Iterator. Есть yield. Функция с yield автоматически становится генератором.
Синтаксис создания генератора Нужно создать класс, реализующий Iterator, и вручную управлять состоянием. Просто написать функцию с yield. Состояние сохраняется автоматически.
Ленивые вычисления Да, если реализовать итератор правильно. Да, по умолчанию. Генераторы всегда ленивые.
Бесконечные последовательности Можно создать, но нужно быть осторожным с hasNext(). Легко создаются через while True: yield x.
Использование в циклах for (T item : iterable) for item in iterable:
Методы для работы hasNext(), next(), remove() __next__(), __iter__(), встроенная функция next()
Исключение при окончании NoSuchElementException StopIteration
Генераторные выражения Нет аналога. Используются стримы (Stream API). Есть: (x*2 for x in range(10))
Производительность Выше, так как Java — компилируемый язык. Ниже, но код короче и понятнее.

Пример на Python для сравнения:

# Python: генератор чисел Фибоначчи
def fibonacci(limit):
    a, b = 0, 1
    for _ in range(limit):
        yield a
        a, b = b, a + b

# Использование
for num in fibonacci(10):
    print(num, end=" ")
# Вывод: 0 1 1 2 3 5 8 13 21 34

Как видите, в Python это занимает 5 строк, а в Java — целый класс. Но Java даёт больше контроля и работает быстрее.

Типичные ошибки и подводные камни

Ошибка 1: Изменение коллекции во время итерации

List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");

for (String s : list) {
    if (s.equals("B")) {
        list.remove(s);  // ConcurrentModificationException!
    }
}

Решение: Используйте явный итератор и его метод remove().

Ошибка 2: Забыли вызвать hasNext()

Iterator<String> it = list.iterator();
it.next();  // Если список пуст — NoSuchElementException!

Решение: Всегда проверяйте hasNext() перед вызовом next().

Ошибка 3: Повторное использование итератора

Iterator<String> it = list.iterator();
while (it.hasNext()) { /* первый проход */ }
while (it.hasNext()) { /* второй проход — не выполнится! */ }

Решение: Для второго прохода запросите новый итератор: list.iterator().

Ошибка 4: Путаница между Iterable и Iterator

Класс должен реализовывать Iterable, чтобы его можно было использовать в for-each. Iterator — это то, что возвращает метод iterator().

Ошибка 5: Неправильная реализация hasNext()

// Неправильно: hasNext() изменяет состояние
public boolean hasNext() {
    return readNextLine() != null;  // Чтение в hasNext() — плохая практика
}

Решение: hasNext() не должен изменять состояние итератора. Он только проверяет, есть ли следующий элемент.

Ошибка 6: Игнорирование checked исключений

Если ваш итератор работает с файлами или сетью, не забывайте обрабатывать IOException.

Лучшие практики и советы профессионалов

  1. Используйте for-each по умолчанию. Он короче, понятнее и безопаснее. Явный итератор используйте только когда нужно удалять элементы или когда нужен ListIterator.
  2. Делайте итераторы неизменяемыми (immutable). Если ваш итератор не поддерживает remove(), бросайте UnsupportedOperationException.
  3. Используйте Stream API для сложных операций. Фильтрация, преобразование, сортировка — стримы делают это элегантно и часто параллельно.
  4. Для больших данных используйте ленивые итераторы. Никогда не загружайте в память то, что можно обработать по частям.
  5. Документируйте поведение итератора. Особенно если он бесконечный, или если он требует закрытия (например, файловый итератор).
  6. Рассмотрите использование сторонних библиотек. Guava от Google содержит множество полезных утилит для работы с итераторами.
  7. Тестируйте граничные случаи: пустая коллекция, коллекция из одного элемента, очень большая коллекция.
  8. Не смешивайте итераторы и стримы. Если начали со стрима — продолжайте стримом. Если начали с итератора — продолжайте итератором. Переключение между ними усложняет код.

Вопросы для самопроверки

  1. Вопрос: В чём разница между интерфейсами Iterable и Iterator?

    Ответ: