Статьи
Утилиты Telegram YouTube Отзывы

Перебор элементов через Iterator

Видеогайд

11 февраля 2024

Тэги: Collections, Java, алгоритмы, руководство.

Содержание

  1. Интерфейс Iterator
  2. Интерфейс Iterable
  3. Изменение коллекции в цикле for-each
  4. Пример реализации интерфейса Iterator
  5. Выводы

Интерфейсы Iterator и Iterable часто используются для работы с коллекциями в Java. Они позволяют эффективно и безопасно работать с коллекциями, обеспечивая контроль над процессом перебора элементов (итерации).

Интерфейс Iterator

Паттерн Итератор

Интерфейс Iterator представляет собой одноимённый шаблон проектирования «Итератор» и содержит несколько методов. Нас интересуют два из них:

interface Iterator<E> {

    boolean hasNext();

    E next();
}
  1. Метод hasNext(), который возвращает true при наличии следующего элемента.
  2. Метод next(), который сдвигает указатель итератора на следующий элемент и возвращает этот элемент в качестве результата. Если доступных элементов не осталось – метод кидает исключение NoSuchElementException.

Пример использования интерфейса Iterator:

List<String> list = new ArrayList<String>();
list.add("Hello");
list.add("World");
Iterator<String> iterator = list.iterator();

while (iterator.hasNext()) {
    String element = iterator.next();
    System.out.println(element);
}

В этом примере мы создаём список строк ArrayList и добавляем в него два элемента. Затем с помощью метода iterator() создаём объект Iterator, который позволяет перебирать элементы списка.

Мы используем цикл while, в котором сначала проверяем наличие следующего элемента методом hasNext(). Если метод возвращает true – идём на итерацию цикла, внутри которого вызываем метод next().

В случае, если мы прошлись по всем элементам, метод hasNext() будет возвращать false и внутрь цикла мы уже не зайдём. При попытке вызвать метод next() после достижения конца списка, мы получим исключение NoSuchElementException.

Каждый итератор обеспечивает перебор только в одну сторону и повторно пройтись этим же итератором по списку не получится.

Интерфейс Iterable

Интерфейс Iterable предоставляет более удобный способ работы с коллекциями, позволяя использовать цикл for-each для итерации по элементам коллекции. Именно он содержит метод iterator(), который возвращает уже знакомый нам объект Iterator.

Также интерфейс содержит методы forEach() и spliterator(). Они уже имеют реализацию по умолчанию, поэтому каждый раз их реализовывать не нужно.

interface Iterable<T> {

    Iterator<T> iterator();

    default void forEach(Consumer<? super T> action) {
        // ...
    }

    default Spliterator<T> spliterator() {
        // ...
    }
}

Пример использования интерфейса Iterable:

List<String> list = new ArrayList<String>();
list.add("Hello");
list.add("World");

for (String element : list) {
    System.out.println(element);
}

В этом примере мы используем цикл for-each для итерации по элементам списка. Внутри цикла неявно вызывается Iterator благодаря тому, что список поддерживает интерфейс Iterable. При этом на каждой итерации будет создаваться новая переменная element, в которую будет помещаться очередной элемент из списка list. Сравните этот пример кода с предыдущим, где мы использовали цикл while. Он стал более компактным и читаемым.

Но можно пойти ещё чуть дальше и вместо цикла for-each использовать метод forEach():

List<String> list = new ArrayList<String>();
list.add("Hello");
list.add("World");

// list.forEach(e -> System.out.println(e));
list.forEach(System.out::println);

Этот пример полностью эквивалентен предыдущему, но является ещё более компактным за счёт использования лямбды. Вся итерация по списку сводится к единственной строке. А поскольку метод println() принимает один параметр, сам вызов лямбды мы также записываем более компактно, через два двоеточия.

Изменение коллекции в цикле for-each

Иногда возникает необходимость при обходе коллекции тут же её модифицировать. Чаще всего требуется удаление элементов, если они удовлетворяют определённым условиям. Первое, что приходит в голову, это сделать удаление прямо внутри цикла for-each:

List<String> list = new ArrayList<>();
list.add("abc");
list.add("def");
list.add("ghi");

for (String s : list) {
    if (s.contains("a")) { // какое-то условие
        list.remove(s); // ConcurrentModificationException
    }
}

Если мы попробуем выполнить такой код, то получим исключение ConcurrentModificationException при попытке удаления элемента. И дело тут совсем не в многопоточности. Дело в том, что внутри итератора стоит проверка, не изменилось ли количество элементов во время обхода коллекции? И если изменилось, то возникает такое исключение.

Решить эту задачу можно по-разному.

Можно с помощью итератора, но это выглядит громоздко и плохо читается. К тому же метод remove() поддерживается не во всех коллекциях.

Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    String s = iterator.next();
    if (s.contains("a")) {
        iterator.remove(); // доступно не во всех типах коллекций
    }
}

Можно с помощью стрима. Читается лучше, но условие придётся инвертировать, что легко забыть сделать. И всё равно многословно:

list = list.stream()
        .filter(e -> !e.contains("a")) // условие инвертировано
        .toList();

Поэтому лучший способ – использование метода removeIf(), который принимает лямбду на вход:

list.removeIf(e -> e.contains("a"));

Так мы легко удаляем элемент из списка в одну строку.

Пример реализации интерфейса Iterator

Предположим, у нас есть собственный класс MyCollection, который никак не связан с иерархией стандартных классов Java Collection. Внутри него содержится массив строк и мы хотели бы перебирать этот массив через цикл for-each, выводя каждую из них на экран.

public class MyCollection {
    private String[] items = new String[]{"aaa", "bbb", "ccc"};

    public static void main(String[] args) {
        var collection = new MyCollection();
        for (String item : collection) {
            System.out.println(item);
        }
    }
}

В данной реализации компилятор будет ругаться на то, что от класса MyCollection требуется реализация интерфейса Iterable. Давайте реализуем его.

public class MyCollection implements Iterable<String> {
    private String[] items = new String[]{"aaa", "bbb", "ccc"};

    @Override
    public Iterator<String> iterator() {
        return new Iterator<>() {
            // реализация методов интерфейса Iterator
        };
    }
}

Как уже упоминалось выше, реализовывать все методы из интерфейса Iterable не нужно. Достаточно реализовать только метод iterator(), который возвращает одноимённый интерфейс. Для краткости реализуем его через анонимный класс:

// анонимный класс внутри метода iterator()
return new Iterator<>() {
    private int index = -1;

    @Override
    public boolean hasNext() {
        return index + 1 < items.length;
    }

    @Override
    public String next() {
        index++;
        if (index >= 0 && index < items.length) {
            return items[index];
        } else {
            throw new NoSuchElementException();
        }
    }
};

Сначала заводим приватное поле index, которое будет хранить индекс текущего элемента и установим его в -1, как заведомо не существующий.

В методе hashNext(), который проверяет наличие следующего элемента, пытаемся прибавлять к текущему индексу единицу и сравниваем с длиной нашего массива. Если полученное значение меньше – возвращаем true. Это значит, что можно вызывать метод next().

Метод next() сначала увеличивает текущий индекс на 1. Затем идёт проверка, попадает ли новый индекс в допустимые границы массива. Если да – возвращаем элемент по этому индексу. Если нет – кидаем исключение NoSuchElementException, согласно контракту интерфейса Iterable.

Вот так выглядит самая простая реализация интерфейса Iterator. Теперь мы можем перебирать элементы из MyCollection в цикле for-each.

Выводы

Для перебора всех элементов коллекции используется интерфейс Iterator. Вручную его методы можно вызывать с помощью цикла while. Если же объект поддерживает интерфейс Iterable, то мы можем использовать цикл for-each, который «под капотом» будет использовать всё тот же Iterator. Альтернативой циклу for-each является метод forEach(), принимающий лямбду, которая будет применена к каждому элементу коллекции.



Комментарии

Добавить комментарий

×

devmark.ru