Одним из ключевых интерфейсов коллекций в языке Java является интерфейс Iterator. Он позволяет проходить по элементам коллекции.
Интерфейс Iterator имеет следующее определение:
public interface Iterator <E>{
boolean hasNext();
E next();
default void remove();
default void forEachRemaining(Consumer<? super E> action);
}
Реализация интерфейса предполагает, что с помощью вызова метода next() можно получить следующий элемент. С помощью
метода hasNext() можно узнать, есть ли следующий элемент, и не достигнут ли конец коллекции. И если элементы еще имеются, то hasNext()
вернет значение true. Метод hasNext() следует вызывать перед методом next(), так как при достижении конца коллекции метод next() выбрасывает исключение
NoSuchElementException. И метод remove() удаляет текущий элемент, который был получен последним вызовом next().
Например, используем итератор для перебора коллекции ArrayList:
import java.util.Collection;
import java.util.ArrayList;
import java.util.Iterator;
public class Program{
public static void main(String[] args) {
Collection<String> people = new ArrayList<String>();
people.add("Tom");
people.add("Bob");
people.add("Sam");
// получаем интератор коллекции people
Iterator<String> iter = people.iterator();
// используя итератор, перебираем коллекцию
while(iter.hasNext()){
System.out.println(iter.next());
}
}
}
Консольный вывод:
Tom Bob Sam
Конечно, для перебора коллекции гораздо проще использовать стандартный цикл for each:
import java.util.Collection;
import java.util.ArrayList;
public class Program{
public static void main(String[] args) {
Collection<String> people = new ArrayList<String>();
people.add("Tom");
people.add("Bob");
people.add("Sam");
for(var p : people){
System.out.println(p);
}
}
}
Однако "под капотом" компилятор преобразует цикл for-each в цикл с итератором.
Интерфейс Iterator предоставляет ограниченный функционал. Но Java имеет встроенные типы, которые расширяют его функционал и предназначены для перебора определенных типов коллекций.
Например, интерфейс ListIterator используется классами, которые реализуют интерфейс List и представляют списки, то есть классами
LinkedList, ArrayList и др.
Интерфейс ListIterator расширяет интерфейс Iterator и определяет ряд дополнительных методов:
void add(E obj): вставляет объект obj перед элементом, который должен быть возвращен следующим вызовом next()
boolean hasNext(): возвращает true, если в коллекции имеется следующий элемент, иначе возвращает false
boolean hasPrevious(): возвращает true, если в коллекции имеется предыдущий элемент, иначе возвращает false
E next(): возвращает текущий элемент и переходит к следующему, если такого нет, то генерируется исключение NoSuchElementException
E previous(): возвращает текущий элемент и переходит к предыдущему, если такого нет, то генерируется исключение NoSuchElementException
int nextIndex(): возвращает индекс следующего элемента. Если такого нет, то возвращается размер списка
int previousIndex(): возвращает индекс предыдущего элемента. Если такого нет, то возвращается число -1
void remove(): удаляет текущий элемент из списка. Таким образом, этот метод должен быть вызван после методов
next() или previous(), иначе будет сгенерировано исключение IlligalStateException
void set(E obj): присваивает текущему элементу, выбранному вызовом методов next() или previous(),
ссылку на объект obj
Используем ListIterator:
import java.util.ListIterator;
import java.util.ArrayList;
public class Program{
public static void main(String[] args) {
var people = new ArrayList<String>();
people.add("Tom");
people.add("Bob");
people.add("Sam");
ListIterator<String> listIter = people.listIterator();
while(listIter.hasNext()){
System.out.println(listIter.next());
}
// сейчас текущий элемент - Sam
// изменим значение этого элемента
listIter.set("Sammy");
// пройдемся по элементам в обратном порядке
while(listIter.hasPrevious()){
System.out.println(listIter.previous());
}
}
}
Консольный вывод программы:
Tom Bob Sam Sammy Bob Tom
Цикл for-each работает с любым объектом, который реализует интерфейс Iterable и реализует его метод iterator():
public interface Iterable<E>
{
Iterator<E> iterator();
....
}
В частности, этот метод возвращает итератор - то есть объект, реализующий интерфейс Iterator. И с помощью этого итератора будет осуществляться перебор объекта класса, которые реализует интерфейс Iterable.
Интерфейс Collection, а через него и все остальные коллекции, как раз расширяет интерфейс Iterable и наследует метод Iterator<E> iterator().:
public interface Collection<E>
{
Iterator<E> iterator();
................
Поэтому вы можете использовать цикл "for each" с любой коллекцией в Java Collections Framework.
Хотя итераторы и интерфейс Iterable предназначены прежде всего для работы с коллекциями, но фактически мы можем их использовать с любыми классами, рассматривая класс как набор некоторых объектов.
Например, реализуем интерфейсы Iterable и Iterator для перебора объекта кастомного класса:
import java.util.Iterator;
public class Program{
public static void main(String[] args) {
Person tom = new Person(22, "Tom", 41);
for(var prop : tom){
System.out.println(prop);
}
}
}
class PersonIterator implements Iterator<String>{
int currentProp = 0;
Person person;
PersonIterator(Person person){ this.person = person; }
public boolean hasNext(){ return currentProp < 3; }
public String next(){
return switch(currentProp++){
case 0 -> String.valueOf(person.getId());
case 1 -> person.getName();
case 2 -> String.valueOf(person.getAge());
default -> null;
};
}
}
class Person implements Iterable{
private int id;
private String name;
private int age;
int getId() { return id;}
String getName() { return name;}
int getAge() { return age;}
private PersonIterator iter = null;
Person(int id, String name, int age){
this.id = id;
this.age = age;
this.name = name;
}
public PersonIterator iterator(){
if(iter == null){
iter = new PersonIterator(this);
}
return iter;
}
}
В данном случае демонстрируется, как сделать собственный класс итерируемым, то есть дать возможность перебирать его в цикле "for-each". И для начала определяем класс, объект которого будет перебираться -
класс Person, который описывает человека:
class Person implements Iterable{
// ... поля и конструктор ...
public PersonIterator iterator(){
if(iter == null){
iter = new PersonIterator(this);
}
return iter;
}
}
Основные моменты:
implements Iterable: реализуя этот интерфейс, класс Person обязуется предоставить метод iterator(), который неявно вызывается циклом "for-each".
public PersonIterator iterator(): этот метод должен вернуть объект, который, в свою очередь, реализует интерфейс Iterator. В данном случае он возвращает экземпляр
нашего кастомного итератора PersonIterator.
Класс PersonIterator содержит всю логику перебора свойств объекта Person:
class PersonIterator implements Iterator<String>{
// ... поля и конструктор ...
public boolean hasNext(){ return currentProp < 3; }
public String next(){
return switch(currentProp++){
// ...
};
}
}
Основные моменты:
implements Iterator<String>: применяем интерфейс, который будет перебирать значения типа String
boolean hasNext(): этот метод вызывается перед каждой итерацией цикла "for-each". Он должен вернуть true, если есть следующий элемент для перебора, и
false, если элементы закончились. Здесь логика проста: поскольку у Person три свойства, метод возвращает true, пока счетчик currentProp меньше 3.
String next(): этот метод возвращает следующий элемент. Он вызывается, если hasNext() вернул true. С помощью оператора switch
он последовательно возвращает значения полей id, name и age в виде строки (String), увеличивая счетчик currentProp на единицу после каждого вызова.
Таким образом, код создает специальную конструкцию в виде PersonIterator о том, как пошагово перебрать свойства объекта Person, и связывает эту конструкцию с самим классом Person через интерфейс
Iterable
И далее в методе main мы можем создать объект класса Person и перебрать его свойства:
public static void main(String[] args) {
Person tom = new Person(22, "Tom", 41);
for(var prop : tom){
System.out.println(prop);
}
}
Результат выполнения программы:
22 Tom 41
Вместо цикла "for each" для перебора коллекции можно вызвать методы Collection.forEach или Iterator.forEachRemaining с лямбда-выражением, которое использует элемент. Лямбда-выражение применяется ко всем элементам коллекции или к оставшимся элементам, которые может обойти итератор:
coll.forEach(element -> действия с element); iter.forEachRemaining(element -> действия с element);
Например:
import java.util.ArrayList;
public class Program{
public static void main(String[] args) {
var people = new ArrayList<String>();
people.add("Tom");
people.add("Bob");
people.add("Sam");
// p представляет каждый объект в people
people.forEach(p -> System.out.println(p) );
}
}