Интерфейсы Iterator и ListIterator в Java - Вопросы

Всего: 6 вопросов

1. 

Что такое Iterator в Java и какие у него методы?

Iterator<E> (пакет java.util, Java 1.2) — интерфейс-курсор для последовательного обхода коллекции, не раскрывающий её внутреннего устройства: один и тот же цикл работает для ArrayList, HashSet и ArrayDeque. Итератор получают методом iterator() из Iterable, который есть у каждой коллекции.

Курсор стоит не на элементе, а между элементами: новый итератор находится перед первым, каждый next() перешагивает через один элемент и возвращает его. Назад обычный Iterator не ходит и сбросить его нельзя — для нового обхода берут новый итератор.

  • boolean hasNext() — есть ли следующий элемент; курсор не двигает.
  • E next() — возвращает следующий элемент; если элементов нет — NoSuchElementException.
  • default void remove() — удаляет элемент, который вернул последний next().
  • default void forEachRemaining(Consumer) (Java 8) — выполняет действие для всех ещё не пройденных элементов.

Стандартный цикл — while (it.hasNext()) ровно с одним вызовом it.next() внутри: два вызова next() за итерацию забирают два элемента и при нечётном количестве бросают NoSuchElementException.

2. 

Как удалить элементы коллекции во время обхода и какие исключения может бросить Iterator.remove()?

Безопасно удалять во время обхода можно только методом remove() самого итератора: он удаляет элемент, который вернул последний next(), и синхронизирует итератор с коллекцией. Вызов list.remove() в том же цикле приводит к ConcurrentModificationException.

List<Integer> numbers = new ArrayList<>(Arrays.asList(1, 2, 3, 4));
Iterator<Integer> it = numbers.iterator();
while (it.hasNext()) {
    if (it.next() % 2 == 0) {
        it.remove();
    }
}
System.out.println(numbers); // [1, 3]
  • IllegalStateException — если remove() вызван до первого next() или дважды подряд: после каждого next() разрешён не более чем один remove().
  • UnsupportedOperationException — у неизменяемых коллекций (например, List.of(...)) и у итераторов, которые не переопределили default-метод remove().

Для удаления по условию без явного цикла есть removeIf() (Java 8); явный итератор нужен, когда при удалении надо сделать что-то ещё — посчитать, залогировать, переложить элемент.

3. 

Почему возникает ConcurrentModificationException и можно ли на него полагаться?

ConcurrentModificationException бросает метод next() итератора, если после создания итератора коллекцию структурно изменили в обход него: вызвали add(), remove() или clear() у самой коллекции. Исключение возникает и в однопоточной программе: concurrent здесь означает «одновременно с обходом», а не «из другого потока».

Механизм: у ArrayList, HashMap и других коллекций java.util есть счётчик структурных изменений modCount. Итератор запоминает его при создании (expectedModCount) и сверяет в каждом next(); при расхождении сразу бросает исключение. Такие итераторы называют fail-fast. Iterator.remove() обновляет оба счётчика, поэтому через него удалять можно. Цикл for-each использует тот же итератор и подчиняется тому же правилу.

Полагаться на исключение нельзя: fail-fast работает «по возможности». Если в for-each удалить предпоследний элемент ArrayList, hasNext() вернёт false раньше проверки в next() — цикл молча завершится и пропустит последний элемент. Изменяйте коллекцию во время обхода только через Iterator.remove(), ListIterator.set()/add() или removeIf(); если нужно добавлять элементы, которые не надо обходить, обходят копию коллекции.

4. 

Чем Iterator отличается от Iterable и как с ними связан цикл for-each?

Iterable<T> (java.lang, Java 5) — то, что перебирают: в нём один абстрактный метод iterator() и default-методы forEach() и spliterator() (Java 8). Iterator<T> (java.util) — то, чем перебирают: одноразовый курсор с состоянием. Один Iterable может выдать сколько угодно независимых итераторов. Collection расширяет Iterable, поэтому for-each работает с любым списком, множеством и очередью.

Для Iterable компилятор разворачивает for-each в обычный обход итератором:

for (String s : list) { ... }
// превращается в
for (Iterator<String> i = list.iterator(); i.hasNext(); ) {
    String s = i.next();
    ...
}

Переменная итератора скрыта, поэтому вызвать remove() в for-each нельзя. Для массивов for-each разворачивается в цикл по индексу. Всё остальное цикл for-each не принимает: код for (String key : map) не скомпилируется, потому что Map не реализует Iterable (обходят keySet(), values() или entrySet()), и передать в for-each сам Iterator тоже нельзя — ошибка for-each not applicable to expression type.

5. 

Как написать свой Iterator и Iterable, чтобы объекты класса можно было перебирать циклом for-each?

Класс реализует Iterable<T>, а его метод iterator() при каждом вызове возвращает новый объект Iterator<T> с методами hasNext() и next():

public class Countdown implements Iterable<Integer> {
    private final int start;

    public Countdown(int start) {
        this.start = start;
    }

    @Override
    public Iterator<Integer> iterator() {
        return new Iterator<Integer>() {
            private int current = start;

            @Override
            public boolean hasNext() {
                return current > 0;
            }

            @Override
            public Integer next() {
                if (!hasNext()) {
                    throw new NoSuchElementException();
                }
                return current--;
            }
        };
    }
}
// for (int n : new Countdown(3)) выведет 3 2 1
  • Новый итератор на каждый вызов iterator() позволяет обходить объект сколько угодно раз; если вернуть одно и то же поле-итератор, второй for-each ничего не выведет.
  • По контракту next() бросает NoSuchElementException, когда элементы кончились, а не возвращает null.
  • remove() реализовывать не обязательно: версия по умолчанию бросает UnsupportedOperationException. forEach() достаётся бесплатно как default-метод Iterable.
  • Анонимный класс с ромбом new Iterator<>() { ... } компилируется только с Java 9; на Java 8 тип указывают явно.
6. 

Чем ListIterator отличается от Iterator и как обойти список с конца?

ListIterator<E> расширяет Iterator и есть только у List (у Set и Queue его нет). К методам Iterator он добавляет шесть своих: hasPrevious() и previous() для обратного хода, nextIndex() и previousIndex() для позиции курсора, set(e) для замены и add(e) для вставки элемента.

Его получают методом listIterator() (курсор перед первым элементом) или listIterator(int index). Вызов listIterator(list.size()) ставит курсор в конец — так начинают обход с конца:

List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
ListIterator<String> it = list.listIterator(list.size());
while (it.hasPrevious()) {
    System.out.print(it.previousIndex() + ":" + it.previous() + " ");
}
// 2:C 1:B 0:A

Правила изменения: set() и remove() относятся к элементу, который вернул последний next() или previous(); после add() или remove() они бросают IllegalStateException, поэтому в одной итерации сначала вызывают set(), потом add(). add() вставляет элемент перед курсором, и следующий next() его не вернёт — вставка на каждой итерации не зацикливает обход. У списка фиксированного размера из Arrays.asList() set() работает, а add() и remove() бросают UnsupportedOperationException.

Страница 1 из 1