Интерфейсы Iterator и ListIterator в Java - Вопросы
Всего: 6 вопросов
1. Что такое Iterator в Java и какие у него методы?
Что такое 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()?
Как удалить элементы коллекции во время обхода и какие исключения может бросить 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 и можно ли на него полагаться?
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?
Чем 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?
Как написать свой 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 отличается от 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.