Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

Свой 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().

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.