Сегодня поговорим о том, что такое Iterator в Java и зачем он нужен. Итератор (Iterator) в Java — это объект, который позволяет последовательно перебирать элементы коллекции, не раскрывая её внутреннюю структуру. Используя методы hasNext() для проверки наличия следующего элемента и next() для его получения, итератор обеспечивает унифицированный способ обхода List, Set и Map. Как ты уже, вероятно, знаешь, в Java есть замечательный интерфейс Collection, реализующий интерфейс Iterator. Сразу оговорюсь, не следует путать интерфейс iterator с паттерном iterator в Java!

Знакомство с интерфейсом Iterator

И дабы внести ясность, для начала разберёмся именно с интерфейсом.
Паттерн Iterator - 1
Дословно «Iterator» можно перевести как «переборщик». То есть это некая сущность, способная перебрать все элементы в коллекции. При этом она позволяет это сделать без вникания во внутреннюю структуру и устройство коллекций.
Представим на секунду, что iterator в Java отсутствует. В таком случае всем и каждому придётся нырнуть в самые глубины коллекций и по-настоящему разобраться, чем отличается, ArrayList от LinkedList и HashSet от TreeSet.

Методы, которые должен имплементировать Iterator

boolean hasNext() — если в итерируемом объекте (пока что это Collection) остались еще значение — метод вернет true, если значения кончились false. E next() — возвращает следующий элемент коллекции (объекта). Если элементов больше нет (не было проверки hasNext(), а мы вызвали next(), достигнув конца коллекции), метод бросит NoSuchElementException. void remove() — удалит элемент, который был в последний раз получен методом next(). Метод может бросить:
  • UnsupportedOperationException, если данный итератор не поддерживает метод remove() (в случае с read-only коллекциями, например)
  • IllegalStateException, если метод next() еще не был вызван, или если remove() уже был вызван после последнего вызова next().

Использование итераторов с коллекциями

Итак, iterator для List — самая распространенная имплементация. Итератор идет от начала коллекции к ее концу: смотрит есть ли в наличии следующий элемент и возвращает его, если таковой находится. На основе этого несложного алгоритма построен цикл for-each. Его расширение — ListIterator. Рассмотрим дополнительные методы java list iterator. Ты их, скорее всего, знаешь:
  • void add(E e) — вставляет элемент E в List;
  • boolean hasPrevious() — вернет true, если при обратном переборе List имеются элементы;
  • int nextIndex()— вернет индекс следующего элемента;
  • E previous() — вернет предыдущий элемент листа;
  • int previousIndex() — вернет индекс предыдущего элемента;
  • void set(E e) — заменит элемент, возвращенный последним вызовом next() или previous() на элемент e.
Рассмотрим небольшой пример. Создадим List, содержащий в себе строки приветствия обучающихся:

List<String> list = new ArrayList<>();
list.add("Привет");
list.add("Обучающимся");
list.add("На");
list.add("JavaRush");
Теперь получим для него итератор и выведем в консоль все содержащиеся строки:

Iterator iterator = list.iterator();
while (iterator.hasNext()) {
    System.out.println(iterator.next());
}
Сейчас будет «узкое место»: Java Collections как ты, вероятно, знаешь (а если не знаешь, разберись), расширяют интерфейс Iterable, но это не означает, что только List, Set и Queue поддерживают итератор. Для java Map iterator также поддерживается, но его необходимо вызывать для Map.entrySet():

Map map = new HashMap<>();
Iterator mapIterator = map.entrySet().iterator();
Iterator> mapIterator = map.entrySet().iterator();
Тогда метод next() будет возвращать объект Entry, содержащий в себе пару «ключ»-«значение». Дальше все аналогично с List:

while (mapIterator.hasNext()) {
    Map.Entry entry = mapIterator.next();
    System.out.println("Key: " + entry.getKey());
    System.out.println("Value: " + entry.getValue());
}

Интерфейс vs. Паттерн: В чем разница?

Ты думаешь: «Стоп. Мы говорим про интерфейс, а в заголовке статьи написано «Паттерн». То есть, паттерн iterator – это интерфейс Iterator? Или интерфейс — это паттерн?» Если это слово встречается впервые, даю справку: паттерн — это шаблон проектирования, некое поведение, которого должен придерживаться класс или множество взаимосвязанных классов. Итератор в java может быть реализован для любого объекта, внутренняя структура которого подразумевает перебор, при этом можно изменить сигнатуру обсуждаемых методов. Главное при реализации паттерна – логика, которой должен придерживаться класс. Интерфейс итератор – частная реализация одноименного паттерна, применяемая как к готовым структурам (List, Set, Queue, Map), так и к прочим, на усмотрение программиста. Расширяя интерфейс Iterator, ты реализуешь паттерн, но для реализации паттерна не обязательно расширять интерфейс. Простая аналогия: все рыбы плавают, но не всё, что плавает – рыбы.

Создание собственного итератора

В качестве примера я решил взять… слово. А конкретнее — существительное. Оно состоит из частей: приставки, корня, суффикса и окончания. Для частей слова создадим интерфейс WordPart и классы, расширяющие его: Prefix, Root, Suffix и Ending:

interface WordPart {
    String getWordPart();
}

static class Root implements WordPart {

    private String part;

    public Root(String part) {
        this.part = part;
    }

    @Override
    public String getWordPart() {
        return part;
    }
}

static class Prefix implements WordPart {

    private String part;

    public Prefix(String part) {
        this.part = part;
    }

    @Override
    public String getWordPart() {
        return part;
    }
}

static class Suffix implements WordPart {

    private String part;

    public Suffix(String part) {
        this.part = part;
    }

    @Override
    public String getWordPart() {
        return part;
    }
}

static class Ending implements WordPart {

    private String part;

    public Ending(String part) {
        this.part = part;
    }

    @Override
    public String getWordPart() {
        return part;
    }
}
Тогда класс Word (слово) будет содержать в себе части, а кроме них добавим целое число, отражающее количество частей в слове:

public class Word {

    private Root root;
    private Prefix prefix;
    private Suffix suffix;
    private Ending ending;
    private int partCount;

    public Word(Root root, Prefix prefix, Suffix suffix, Ending ending) {
        this.root = root;
        this.prefix = prefix;
        this.suffix = suffix;
        this.ending = ending;
        this.partCount = 4;
    }

    public Word(Root root, Prefix prefix, Suffix suffix) {
        this.root = root;
        this.prefix = prefix;
        this.suffix = suffix;
        this.partCount = 3;

    }

    public Word(Root root, Prefix prefix) {
        this.root = root;
        this.prefix = prefix;
        this.partCount = 2;
    }

    public Word(Root root) {
        this.root = root;
        this.partCount = 1;
    }

    public Root getRoot() {
        return root;
    }

    public Prefix getPrefix() {
        return prefix;
    }

    public Suffix getSuffix() {
        return suffix;
    }

    public Ending getEnding() {
        return ending;
    }

    public int getPartCount() {
        return partCount;
    }

    public boolean hasRoot() {
        return this.root != null;
    }

    public boolean hasPrefix() {
        return this.prefix != null;
    }

    public boolean hasSuffix() {
        return this.suffix != null;
    }

    public boolean hasEnding() {
        return this.ending != null;
    }

Окей, у нас есть четыре перегруженных конструктора (для простоты, предположим, что суффикс у нас может быть только один). Существительное не может состоять из одной приставки, поэтому для конструктора с одним параметром будем устанавливать корень. Теперь напишем реализацию паттерна итератор: WordIterator, переопределяющий 2 метода: hasNext() и next():

import java.util.Iterator;
import java.util.NoSuchElementException;

public class WordIterator implements Iterator {

    private final Word word;
    private int currentPartIndex;

    public WordIterator(Word word) {
        this.word = word;
        this.currentPartIndex = 0;
    }

    @Override
    public boolean hasNext() {
        return currentPartIndex < word.getPartCount();
    }

    @Override
    public Word.WordPart next() {
        if (!hasNext()) {
            throw new NoSuchElementException("Больше нет частей в этом слове!");
        }
        
        // Задаем обратный порядок, как в оригинальной статье: окончание -> суффикс -> приставка -> корень
        int partToReturnIndex = word.getPartCount() - 1 - currentPartIndex;
        currentPartIndex++;

        switch (partToReturnIndex) {
            case 3:
                return word.getEnding();
            case 2:
                return word.getSuffix();
            case 1:
                return word.getPrefix();
            case 0:
                return word.getRoot();
            default:
                 // Эта логика зависит от того, как устроен класс Word и его конструкторы
                 // Для простоты будем отталкиваться от корня при меньшем кол-ве частей
                 if (word.hasRoot()) return word.getRoot();
                 if (word.hasPrefix()) return word.getPrefix();
                 if (word.hasSuffix()) return word.getSuffix();
                 return word.getEnding();
        }
    }

    @Override
    public void remove() {
        throw new UnsupportedOperationException("Удаление частей слова не поддерживается!");
    }
}
Осталось назначить итератор классу Word:

public class Word implements Iterable {
	//…
	@Override
	public Iterator iterator() {
    		return new WordIterator(this);
	}
	//…
}
Теперь проведем морфемный разбор слова «перебежка»:

public class Main {
    public static void main(String[] args) {
        Word.Root root = new Word.Root("беж");
        Word.Prefix prefix = new Word.Prefix("пере");
        Word.Suffix suffix = new Word.Suffix("к");
        Word.Ending ending = new Word.Ending("a");
        Word word = new Word(root, prefix, suffix, ending);

        Iterator wordIterator = word.iterator();
        while (wordIterator.hasNext()) {
            Word.WordPart part = wordIterator.next();
            System.out.println(part.getClass().getSimpleName() + ": " + part.getWordPart());
        }
    }
}
Обрати внимание, в своей реализации паттерна iterator я выбрал следующий порядок вывода:
  1. окончание
  2. суффикс
  3. приставка
  4. корень
При проектировании собственного итератора, ты сам можешь задать алгоритм итерации, как захочешь. Успехов в обучении!