11 февраля 2024
Тэги: Collections, Java, алгоритмы, руководство.
Интерфейсы Iterator и Iterable часто используются для работы с коллекциями в Java. Они позволяют эффективно и безопасно работать с коллекциями, обеспечивая контроль над процессом перебора элементов (итерации).
Интерфейс Iterator представляет собой одноимённый шаблон проектирования «Итератор» и содержит несколько методов. Нас интересуют два из них:
Пример использования интерфейса Iterator:
В этом примере мы создаём список строк ArrayList и добавляем в него два элемента. Затем с помощью метода iterator() создаём объект Iterator, который позволяет перебирать элементы списка.
Мы используем цикл while, в котором сначала проверяем наличие следующего элемента методом hasNext(). Если метод возвращает true – идём на итерацию цикла, внутри которого вызываем метод next().
В случае, если мы прошлись по всем элементам, метод hasNext() будет возвращать false и внутрь цикла мы уже не зайдём. При попытке вызвать метод next() после достижения конца списка, мы получим исключение NoSuchElementException.
Каждый итератор обеспечивает перебор только в одну сторону и повторно пройтись этим же итератором по списку не получится.
Интерфейс Iterable предоставляет более удобный способ работы с коллекциями, позволяя использовать цикл for-each для итерации по элементам коллекции. Именно он содержит метод iterator(), который возвращает уже знакомый нам объект Iterator.
Также интерфейс содержит методы forEach() и spliterator(). Они уже имеют реализацию по умолчанию, поэтому каждый раз их реализовывать не нужно.
Пример использования интерфейса Iterable:
В этом примере мы используем цикл for-each для итерации по элементам списка. Внутри цикла неявно вызывается Iterator благодаря тому, что список поддерживает интерфейс Iterable. При этом на каждой итерации будет создаваться новая переменная element, в которую будет помещаться очередной элемент из списка list. Сравните этот пример кода с предыдущим, где мы использовали цикл while. Он стал более компактным и читаемым.
Но можно пойти ещё чуть дальше и вместо цикла for-each использовать метод forEach():
Этот пример полностью эквивалентен предыдущему, но является ещё более компактным за счёт использования лямбды. Вся итерация по списку сводится к единственной строке. А поскольку метод println() принимает один параметр, сам вызов лямбды мы также записываем более компактно, через два двоеточия.
Иногда возникает необходимость при обходе коллекции тут же её модифицировать. Чаще всего требуется удаление элементов, если они удовлетворяют определённым условиям. Первое, что приходит в голову, это сделать удаление прямо внутри цикла for-each:
Если мы попробуем выполнить такой код, то получим исключение ConcurrentModificationException при попытке удаления элемента. И дело тут совсем не в многопоточности. Дело в том, что внутри итератора стоит проверка, не изменилось ли количество элементов во время обхода коллекции? И если изменилось, то возникает такое исключение.
Решить эту задачу можно по-разному.
Можно с помощью итератора, но это выглядит громоздко и плохо читается. К тому же метод remove() поддерживается не во всех коллекциях.
Можно с помощью стрима. Читается лучше, но условие придётся инвертировать, что легко забыть сделать. И всё равно многословно:
Поэтому лучший способ – использование метода removeIf(), который принимает лямбду на вход:
Так мы легко удаляем элемент из списка в одну строку.
Предположим, у нас есть собственный класс MyCollection, который никак не связан с иерархией стандартных классов Java Collection. Внутри него содержится массив строк и мы хотели бы перебирать этот массив через цикл for-each, выводя каждую из них на экран.
В данной реализации компилятор будет ругаться на то, что от класса MyCollection требуется реализация интерфейса Iterable. Давайте реализуем его.
Как уже упоминалось выше, реализовывать все методы из интерфейса Iterable не нужно. Достаточно реализовать только метод iterator(), который возвращает одноимённый интерфейс. Для краткости реализуем его через анонимный класс:
Сначала заводим приватное поле index, которое будет хранить индекс текущего элемента и установим его в -1, как заведомо не существующий.
В методе hashNext(), который проверяет наличие следующего элемента, пытаемся прибавлять к текущему индексу единицу и сравниваем с длиной нашего массива. Если полученное значение меньше – возвращаем true. Это значит, что можно вызывать метод next().
Метод next() сначала увеличивает текущий индекс на 1. Затем идёт проверка, попадает ли новый индекс в допустимые границы массива. Если да – возвращаем элемент по этому индексу. Если нет – кидаем исключение NoSuchElementException, согласно контракту интерфейса Iterable.
Вот так выглядит самая простая реализация интерфейса Iterator. Теперь мы можем перебирать элементы из MyCollection в цикле for-each.
Для перебора всех элементов коллекции используется интерфейс Iterator. Вручную его методы можно вызывать с помощью цикла while. Если же объект поддерживает интерфейс Iterable, то мы можем использовать цикл for-each, который «под капотом» будет использовать всё тот же Iterator. Альтернативой циклу for-each является метод forEach(), принимающий лямбду, которая будет применена к каждому элементу коллекции.
Kotlin, Java, Spring, Spring Boot, Spring Data, SQL, PostgreSQL, Oracle, Linux, Hibernate, Collections, Stream API, многопоточность, файлы, Nginx, Apache, maven, gradle, JUnit, YouTube, новости, руководство, ООП, алгоритмы, головоломки, rest, GraphQL, Excel, XML, json, yaml.