Свой Iterable
Цикл for-each работает не только со стандартными коллекциями. Он принимает массив или любой объект, класс которого реализует интерфейс Iterable. Значит, свой класс тоже можно научить перебираться в for-each.
Интерфейс Iterable
У интерфейса Iterable<T> один обязательный метод — iterator(). Он возвращает итератор: объект, который выдаёт элементы по одному. Если класс хранит элементы в списке, достаточно вернуть итератор этого списка:
class Playlist implements Iterable<String> {
private final List<String> songs = new ArrayList<>();
void add(String song) {
songs.add(song);
}
public Iterator<String> iterator() {
return songs.iterator();
}
}
void main() {
Playlist playlist = new Playlist();
playlist.add("Утро");
playlist.add("Дорога");
for (String song : playlist) {
IO.println(song);
}
}Результат:
Утро
ДорогаСнаружи список songs не виден, но перебрать плейлист можно — инкапсуляция не нарушена.
Свой итератор
Иногда элементы нигде не хранятся, а вычисляются на ходу. Тогда итератор пишут сами. У интерфейса Iterator<T> два метода: hasNext() отвечает, есть ли ещё элементы, а next() возвращает очередной:
record Range(int from, int to) implements Iterable<Integer> {
public Iterator<Integer> iterator() {
return new Iterator<>() {
private int current = from;
public boolean hasNext() {
return current <= to;
}
public Integer next() {
return current++;
}
};
}
}
void main() {
for (int n : new Range(3, 6)) {
IO.print(n + " ");
}
}Результат:
3 4 5 6 Итератор здесь — анонимный класс. Он хранит своё положение в поле current, поэтому каждый новый цикл начинает обход заново.
Что делает for-each
Цикл for-each — сокращённая запись. Компилятор превращает его в такой цикл с итератором:
void main() {
List<String> names = List.of("Анна", "Олег");
Iterator<String> it = names.iterator();
while (it.hasNext()) {
String name = it.next();
IO.println(name);
}
}Результат:
Анна
ОлегПоэтому for-each и умеет работать с любым Iterable: ему нужны только hasNext() и next().
Правила для next()
Если элементы закончились, next() должен выбросить NoSuchElementException — так ведут себя все итераторы Java. Добавим проверку в Range:
record Range(int from, int to) implements Iterable<Integer> {
public Iterator<Integer> iterator() {
return new Iterator<>() {
private int current = from;
public boolean hasNext() {
return current <= to;
}
public Integer next() {
if (!hasNext()) {
throw new NoSuchElementException("Диапазон закончился");
}
return current++;
}
};
}
}
void main() {
Iterator<Integer> it = new Range(1, 1).iterator();
IO.println(it.next());
IO.println(it.next());
}Результат:
1
Exception in thread "main" java.util.NoSuchElementException: Диапазон закончилсяforEach в подарок
Реализовав Iterable, класс бесплатно получает метод forEach() — он уже написан в самом интерфейсе:
record Range(int from, int to) implements Iterable<Integer> {
public Iterator<Integer> iterator() {
return new Iterator<>() {
private int current = from;
public boolean hasNext() {
return current <= to;
}
public Integer next() {
return current++;
}
};
}
}
void main() {
new Range(1, 3).forEach(n -> IO.println("Шаг " + n));
}Результат:
Шаг 1
Шаг 2
Шаг 3Частая ошибка
Если класс хранит список, но не реализует Iterable, перебрать его объект в for-each не получится — компилятор не знает, откуда брать элементы:
class Team {
List<String> members = List.of("Анна", "Олег");
}
void main() {
for (String name : new Team()) {
IO.println(name);
}
}Результат:
Main.java:6: error: for-each not applicable to expression type
for (String name : new Team()) {
^
required: array or java.lang.Iterable
found: Main.Team
1 error
error: compilation failedДобавьте implements Iterable<String> и метод iterator(), который возвращает members.iterator().