1. Обзор
В этом руководстве мы рассмотрим использование интерфейсов Iterable
и Iterator
в Java и различия между ними.
2. Итерируемый
интерфейс
Итерируемый
интерфейс принадлежит пакету java.lang
. Он представляет собой структуру данных, которую можно повторять.
Интерфейс Iterable
предоставляет метод, создающий Iterator
. При использовании Iterable
мы не можем получить элемент по индексу. Точно так же мы не можем получить первый или последний элементы из структуры данных.
Все коллекции в Java реализуют интерфейс Iterable .
2.1. Итерация по Iterable
Мы можем перебирать элементы внутри коллекции, используя расширенный цикл for
, также называемый циклом for
-each . Однако в таком операторе могут использоваться только объекты, реализующие интерфейс Iterable .
Также можно перебирать элементы, используя оператор while
в сочетании с Iterator
.
Давайте посмотрим на пример перебора элементов в списке
с использованием оператора for
-each:
List<Integer> numbers = getNumbers();
for (Integer number : numbers) {
System.out.println(number);
}
Точно так же мы можем использовать метод forEach()
в сочетании с лямбда-выражениями:
List<Integer> numbers = getNumbers();
numbers.forEach(System.out::println);
2.2. Реализация итерируемого
интерфейса
Пользовательские реализации интерфейса Iterable
могут пригодиться, когда у нас есть собственные структуры данных, которые мы хотели бы повторить.
Начнем с создания класса, представляющего корзину для покупок, которая будет содержать элементы массива. Мы не будем вызывать цикл for
-each непосредственно для массива. Вместо этого мы реализуем интерфейс Iterable .
Мы не хотим, чтобы наши клиенты зависели от выбранной структуры данных. Если мы предоставим клиентам возможность выполнять итерации, мы сможем легко использовать другую структуру данных, и клиентам не придется менять код.
Класс ShoppingCart
реализует интерфейс Iterable
и переопределяет его метод iterate()
:
public class ShoppingCart<E> implements Iterable<E> {
private E[] elementData;
private int size;
public void add(E element) {
ensureCapacity(size + 1);
elementData[size++] = element;
}
@Override
public Iterator<E> iterator() {
return new ShoppingCartIterator();
}
}
Метод add()
сохраняет элементы в массиве. Из-за фиксированного размера и емкости массива мы увеличиваем максимальное количество элементов с помощью метода sureCapacity()
.
Каждый вызов метода iterator()
для пользовательской структуры данных создает новый экземпляр Iterator
. Мы создаем новый экземпляр, поскольку итератор отвечает за поддержание текущего состояния итерации.
Предоставляя конкретную реализацию метода iterator()
, мы можем использовать расширенный оператор for
для перебора объектов реализованного класса.
Теперь давайте создадим внутренний класс внутри класса ShoppingCart
, который представляет наш пользовательский итератор:
public class ShoppingCartIterator implements Iterator<E> {
int cursor;
int lastReturned = -1;
public boolean hasNext() {
return cursor != size;
}
public E next() {
return getNextElement();
}
private E getNextElement() {
int current = cursor;
exist(current);
E[] elements = ShoppingCart.this.elementData;
validate(elements, current);
cursor = current + 1;
lastReturned = current;
return elements[lastReturned];
}
}
Наконец, давайте создадим экземпляр нашего итерируемого класса и используем его в расширенном цикле for :
ShoppingCart<Product> shoppingCart = new ShoppingCart<>();
shoppingCart.add(new Product("Tuna", 42));
shoppingCart.add(new Product("Eggplant", 65));
shoppingCart.add(new Product("Salad", 45));
shoppingCart.add(new Product("Banana", 29));
for (Product product : shoppingCart) {
System.out.println(product.getName());
}
3. Интерфейс итератора
Iterator
является членом Java Collections Framework. Он принадлежит пакету java.util
. Этот интерфейс позволяет нам извлекать или удалять элементы из коллекции во время итерации.
Кроме того, у него есть два метода, которые помогают перебирать структуру данных и извлекать ее элементы — next()
и hasNext()
.
Более того, у него есть метод remove()
, который удаляет текущий элемент, на который указывает Iterator
.
Наконец, метод forEachRemaining(Consumer<? super E> action)
выполняет заданное действие для каждого оставшегося элемента внутри структуры данных.
3.1. Итерация по коллекции
Давайте посмотрим, как перебирать список
элементов Integer
. В примере мы объединим цикл while
и методы hasNext()
и next()
.
Интерфейс List
является частью Collection
и, следовательно, расширяет интерфейс Iterable .
Чтобы получить итератор из коллекции, нам просто нужно вызвать метод iterator()
:
List<Integer> numbers = new ArrayList<>();
numbers.add(10);
numbers.add(20);
numbers.add(30);
numbers.add(40);
Iterator<Integer> iterator = numbers.iterator();
Кроме того, мы можем проверить, есть ли в итераторе оставшиеся элементы, вызвав метод hasNext()
. После этого мы можем получить элемент, вызвав метод next()
:
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
Метод next()
возвращает следующий элемент из итерации. С другой стороны, если такого элемента нет, выбрасывается NoSuchElementException
.
3.2. Реализация интерфейса итератора
Теперь мы реализуем интерфейс Iterator
. Пользовательская реализация может быть полезна, когда нам нужно перебрать коллекцию, используя условный поиск элементов. Например, мы можем использовать собственный итератор для перебора нечетных или четных чисел.
Чтобы проиллюстрировать это, мы будем перебирать простые числа из данной коллекции. Как известно, число считается простым, если оно делится только на единицу и само на себя.
Во-первых, давайте создадим класс, содержащий набор числовых элементов:
class Numbers {
private static final List<Integer> NUMBER_LIST =
Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
}
Кроме того, давайте определим конкретную реализацию интерфейса Iterator
:
private static class PrimeIterator implements Iterator<Integer> {
private int cursor;
@Override
public Integer next() {
exist(cursor);
return NUMBER_LIST.get(cursor++);
}
@Override
public boolean hasNext() {
if (cursor > NUMBER_LIST.size()) {
return false;
}
for (int i = cursor; i < NUMBER_LIST.size(); i++) {
if (isPrime(NUMBER_LIST.get(i))) {
cursor = i;
return true;
}
}
return false;
}
}
Конкретные реализации обычно создаются как внутренние классы. Кроме того, они отвечают за поддержание текущего состояния итерации. В приведенном выше примере мы сохранили текущую позицию следующего простого числа внутри переменной экземпляра. Каждый раз, когда мы вызываем метод next()
, переменная будет содержать индекс предстоящего простого числа.
Любая реализация метода next()
должна генерировать исключение NoSuchElementException
, когда больше не осталось элементов. В противном случае итерация может привести к неожиданному поведению.
Давайте определим метод внутри класса Number
, который возвращает новый экземпляр класса PrimeIterator
:
public static Iterator<Integer> iterator() {
return new PrimeIterator();
}
Наконец, мы можем использовать наш собственный итератор в операторе while :
Iterator<Integer> iterator = Numbers.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
4. Различия между Iterable
и Iterator
Подводя итог, в следующей таблице показаны основные различия между интерфейсами Iterable
и Iterator
:
| Итерируемый | Итератор |
| Представляет коллекцию, которую можно перебирать с помощью цикла `for` -each. | Представляет интерфейс, который можно использовать для перебора коллекции. |
| При реализации `Iterable` нам нужно переопределить метод iterator `() .` | При реализации `Iterator` нам нужно переопределить методы `hasNext()` и `next() .` |
| Не сохраняет состояние итерации | Сохраняет состояние итерации |
| Удаление элементов во время итерации запрещено | Удаление элементов во время итерации разрешено |
5. Вывод
В этой статье мы рассмотрели различия между интерфейсами Iterable
и Iterator
в Java и их использование.
Как всегда, исходный код примеров доступен на GitHub .