Iterable, Iterator и for-each
Разбираемся, как Java обходит коллекции: интерфейс Iterable, объект Iterator, цикл for-each под капотом и безопасное удаление элементов
Зачем нужен обход коллекций
В предыдущих уроках мы научились создавать коллекции и добавлять в них элементы. Но как их прочитать? Как пройтись по каждому элементу и что-то с ним сделать?
Представьте, что у вас есть коробка с карточками. Чтобы прочитать все карточки, вы берёте одну за другой, читаете и кладёте обратно. Итератор (Iterator) в Java работает именно так — он проходит по коллекции элемент за элементом.
Java предоставляет три способа обхода коллекций: интерфейс Iterable с методом iterator(), класс Iterator с методами hasNext/next, и цикл for-each (syntactic sugar для Iterator). Давайте разберём каждый из них подробно.
Интерфейс Iterable
Интерфейс Iterable — это базовый интерфейс для всех коллекций, которые можно обходить. Он находится в пакете java.lang и содержит всего один метод — iterator(), который возвращает объект Iterator.
Почти все коллекции Java (ArrayList, LinkedList, HashSet, HashMap, TreeMap и т.д.) реализуют интерфейс Iterable. Это значит, что вы можете использовать их в цикле for-each.
import java.util.ArrayList;
import java.util.Iterator;
public class Main {
public static void main(String[] args) {
ArrayList<String> fruits = new ArrayList<>();
fruits.add("Яблоко");
fruits.add("Банан");
fruits.add("Вишня");
Iterator<String> iterator = fruits.iterator();
while (iterator.hasNext()) {
String fruit = iterator.next();
System.out.println(fruit);
}
}
}
Метод iterator() возвращает объект Iterator, который знает, как проходить по элементам коллекции. У Iterator есть два основных метода:
- hasNext() — возвращает true, если есть ещё элементы для чтения
- next() — возвращает следующий элемент и сдвигает курсор вперёд
Аналогия: Iterator — как страница книги. hasNext() проверяет, есть ли ещё страницы. next() — переворачивает страницу и возвращает её содержимое.
Интерфейс Iterator подробно
Интерфейс Iterator определён в пакете java.util и имеет три метода:
public interface Iterator<E> {
boolean hasNext();
E next();
void remove();
}
Давайте рассмотрим каждый метод подробно.
hasNext() — проверяет, есть ли ещё элементы. Возвращает boolean. Не перемещает курсор — это просто «подглядывание» вперёд:
ArrayList<String> list = new ArrayList<>();
list.add("А");
list.add("Б");
Iterator<String> it = list.iterator();
System.out.println(it.hasNext()); // true (есть "А")
System.out.println(it.hasNext()); // true (всё ещё есть "А")
System.out.println(it.hasNext()); // true (всё ещё "А", курсор не двигается)
next() — возвращает следующий элемент и сдвигает курсор:
ArrayList<String> list = new ArrayList<>();
list.add("А");
list.add("Б");
list.add("В");
Iterator<String> it = list.iterator();
String first = it.next(); // "А", курсор теперь между "А" и "Б"
String second = it.next(); // "Б", курсор между "Б" и "В"
String third = it.next(); // "В", курсор после "В"
System.out.println(first + " " + second + " " + third);
// А Б В
Если вызвать next(), когда элементов больше нет, выбросится NoSuchElementException.
remove() — удаляет текущий элемент. Подробнее об этом мы поговорим позже.
Цикл for-each: сахарный синтаксис
Писать Iterator вручную — утомительно. Поэтому Java предоставляет цикл for-each (также называемый enhanced for loop), который делает то же самое, но с гораздо более чистым синтаксисом:
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList<String> fruits = new ArrayList<>();
fruits.add("Яблоко");
fruits.add("Банан");
fruits.add("Вишня");
for (String fruit : fruits) {
System.out.println(fruit);
}
}
}
Конструкция for (String fruit : fruits) читается как «для каждого фрукта в списке фруктов». Это гораздо чище, чем ручной Iterator.
Под капотом for-each делает ровно то же самое, что и Iterator. Компилятор превращает for-each в обычный цикл с Iterator:
for (String fruit : fruits) {
System.out.println(fruit);
}
// Компилятор превращает это в:
Iterator<String> iterator = fruits.iterator();
while (iterator.hasNext()) {
String fruit = iterator.next();
System.out.println(fruit);
}
Давайте рассмотрим ещё несколько примеров for-each:
import java.util.ArrayList;
import java.util.HashSet;
public class Main {
public static void main(String[] args) {
ArrayList<Integer> numbers = new ArrayList<>();
numbers.add(10);
numbers.add(20);
numbers.add(30);
int sum = 0;
for (int num : numbers) {
sum += num;
}
System.out.println("Сумма: " + sum); // 60
HashSet<String> colors = new HashSet<>();
colors.add("Красный");
colors.add("Синий");
colors.add("Зелёный");
for (String color : colors) {
System.out.println("Цвет: " + color);
}
int[] array = {1, 2, 3, 4, 5};
for (int value : array) {
System.out.println(value);
}
}
}
Обратите внимание — for-each работает не только с коллекциями, но и с массивами! Это очень удобно.
ConcurrentModificationException
Теперь давайте поговорим о важной проблеме: что происходит, если вы пытаетесь удалить элемент из коллекции во время обхода?
Представьте, что вы читаете список гостей на вечеринке и решаете удалить кого-то из списка, пока читаете. Это опасно — вы можете пропустить кого-то или обратиться к уже удалённому элементу. Java защищает от таких ошибок, выбрасывая ConcurrentModificationException.
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList<String> fruits = new ArrayList<>();
fruits.add("Яблоко");
fruits.add("Банан");
fruits.add("Вишня");
for (String fruit : fruits) {
if (fruit.equals("Банан")) {
fruits.remove(fruit); // ConcurrentModificationException!
}
}
}
}
Эта программа выбросит ConcurrentModificationException, потому что мы пытаемся изменить коллекцию (удалить элемент) во время обхода через for-each (который использует Iterator).
Та же проблема возникает при попытке добавить элемент во время обхода:
for (String fruit : fruits) {
if (fruit.equals("Яблоко")) {
fruits.add("Манго"); // Тоже ConcurrentModificationException!
}
}
Правило: Никогда не изменяйте коллекцию (не добавляйте и не удаляйте элементы) во время обхода через for-each. Если нужно удалить элементы во время обхода — используйте Iterator с его методом remove().
Безопасное удаление: iterator.remove()
Метод remove() у Iterator позволяет безопасно удалить текущий элемент во время обхода. Это работает, потому что Iterator отслеживает состояние коллекции и обновляет внутренний счётчик модификаций.
import java.util.ArrayList;
import java.util.Iterator;
public class Main {
public static void main(String[] args) {
ArrayList<String> fruits = new ArrayList<>();
fruits.add("Яблоко");
fruits.add("Банан");
fruits.add("Вишня");
fruits.add("Манго");
Iterator<String> iterator = fruits.iterator();
while (iterator.hasNext()) {
String fruit = iterator.next();
if (fruit.equals("Банан") || fruit.equals("Манго")) {
iterator.remove();
}
}
System.out.println(fruits); // [Яблоко, Вишня]
}
}
Важно: сначала вызывается next(), потом remove(). Если вызвать remove() без предварительного next(), получите IllegalStateException.
Правильный порядок вызовов:
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String element = it.next(); // 1. Сначала получаем элемент
if (condition) {
it.remove(); // 2. Потом удаляем
}
}
// НЕПРАВИЛЬНО:
it.remove(); // IllegalStateException! Нет текущего элемента
ListIterator: двусторонний обход
Обычный Iterator работает только вперёд — от начала к концу. Но иногда нужно обходить список в обратном направлении или изменять элементы на лету. Для этого существует ListIterator — расширенная версия Iterator для списков.
ListIterator имеет больше возможностей, чем обычный Iterator:
- hasPrevious() — проверяет, есть ли элементы قبل текущей позиции
- previous() — возвращает предыдущий элемент и сдвигает курсор назад
- set(element) — заменяет текущий элемент новым
- add(element) — вставляет новый элемент перед текущей позицией
- nextIndex() — возвращает индекс следующего элемента
- previousIndex() — возвращает индекс предыдущего элемента
Обход вперёд:
import java.util.ArrayList;
import java.util.ListIterator;
public class Main {
public static void main(String[] args) {
ArrayList<String> fruits = new ArrayList<>();
fruits.add("Яблоко");
fruits.add("Банан");
fruits.add("Вишня");
ListIterator<String> it = fruits.listIterator();
while (it.hasNext()) {
int index = it.nextIndex();
String fruit = it.next();
System.out.println(index + ": " + fruit);
}
}
}
Результат: 0: Яблоко, 1: Банан, 2: Вишня.
Обход назад:
ArrayList<String> fruits = new ArrayList<>();
fruits.add("Яблоко");
fruits.add("Банан");
fruits.add("Вишня");
ListIterator<String> it = fruits.listIterator(fruits.size());
while (it.hasPrevious()) {
int index = it.previousIndex();
String fruit = it.previous();
System.out.println(index + ": " + fruit);
}
// 2: Вишня
// 1: Банан
// 0: Яблоко
Обратите внимание — мы создаём ListIterator с начальной позицией fruits.size(), чтобы начать с конца списка.
Замена элементов через set():
ArrayList<String> fruits = new ArrayList<>();
fruits.add("Яблоко");
fruits.add("Банан");
fruits.add("Вишня");
ListIterator<String> it = fruits.listIterator();
while (it.hasNext()) {
String fruit = it.next();
it.set(fruit.toUpperCase());
}
System.out.println(fruits); // [ЯБЛОКО, БАНАН, ВИШНЯ]
Метод set() заменяет последний элемент, возвращённый next() или previous(), на новый. Это безопасно — коллекция не выбросит ConcurrentModificationException.
Вставка элементов через add():
ArrayList<String> fruits = new ArrayList<>();
fruits.add("Яблоко");
fruits.add("Вишня");
ListIterator<String> it = fruits.listIterator();
while (it.hasNext()) {
String fruit = it.next();
if (fruit.equals("Вишня")) {
it.add("Банан");
}
}
System.out.println(fruits); // [Яблоко, Банан, Вишня]
Обход разных коллекций
Давайте посмотрим, как обходить разные типы коллекций. Все они реализуют интерфейс Iterable, поэтому for-each работает одинаково:
import java.util.ArrayList;
import java.util.HashSet;
import java.util.HashMap;
import java.util.LinkedList;
public class Main {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("A");
list.add("B");
for (String s : list) {
System.out.println("List: " + s);
}
LinkedList<Integer> linked = new LinkedList<>();
linked.add(1);
linked.add(2);
for (Integer n : linked) {
System.out.println("LinkedList: " + n);
}
HashSet<String> set = new HashSet<>();
set.add("X");
set.add("Y");
for (String s : set) {
System.out.println("Set: " + s);
}
HashMap<String, Integer> map = new HashMap<>();
map.put("А", 1);
map.put("Б", 2);
for (String key : map.keySet()) {
System.out.println("Map: " + key + " = " + map.get(key));
}
}
}
Практический пример: фильтрация списка
Давайте создадим программу, которая удаляет из списка все чётные числа, используя Iterator:
import java.util.ArrayList;
import java.util.Iterator;
public class FilterEven {
public static void main(String[] args) {
ArrayList<Integer> numbers = new ArrayList<>();
numbers.add(1);
numbers.add(2);
numbers.add(3);
numbers.add(4);
numbers.add(5);
numbers.add(6);
System.out.println("До: " + numbers);
Iterator<Integer> it = numbers.iterator();
while (it.hasNext()) {
Integer num = it.next();
if (num % 2 == 0) {
it.remove();
}
}
System.out.println("После: " + numbers);
// До: [1, 2, 3, 4, 5, 6]
// После: [1, 3, 5]
}
}
Сравнение способов обхода
Давайте подведём итог и сравним три способа обхода коллекций:
| Способ | Удобство | Возможности |
|---|---|---|
| for-each | Очень удобный | Только чтение |
| Iterator | Удобный | Чтение + удаление |
| ListIterator | Средний | Чтение + удаление + замена + вставка + обратный обход |
Правило выбора: Используйте for-each, если просто нужно прочитать элементы. Используйте Iterator, если нужно удалять элементы во время обхода. Используйте ListIterator, если нужен обратный обход или замена элементов.
Создание自己的 Iterable класса
Вы можете создать собственный класс, который реализует интерфейс Iterable, и использовать его в цикле for-each. Это полезно, когда вы создаёте собственную структуру данных.
import java.util.Iterator;
import java.util.NoSuchElementException;
class NumberRange implements Iterable<Integer> {
private int start;
private int end;
NumberRange(int start, int end) {
this.start = start;
this.end = end;
}
@Override
public Iterator<Integer> iterator() {
return new Iterator<Integer>() {
private int current = start;
@Override
public boolean hasNext() {
return current <= end;
}
@Override
public Integer next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
return current++;
}
};
}
}
public class Main {
public static void main(String[] args) {
NumberRange range = new NumberRange(1, 5);
for (int num : range) {
System.out.println(num);
}
}
}
Этот класс позволяет итерировать по диапазону чисел от 1 до 5. Вы можете использовать его в цикле for-each, хотя он не использует ArrayList или другую стандартную коллекцию.
Итоги урока
- Интерфейс Iterable определяет метод iterator(), который возвращает Iterator
- Iterator имеет методы hasNext() (проверка наличия элементов) и next() (получение следующего)
- Цикл for-each — это удобная замена ручному Iterator (syntactic sugar)
- Под капотом for-each использует Iterator
- ConcurrentModificationException выбрасывается при изменении коллекции во время обхода
- Для безопасного удаления используйте iterator.remove() вместо collection.remove()
- ListIterator расширяет Iterator: добавляет previous(), hasPrevious(), set(), add()
- ListIterator позволяет обходить список в обратном направлении
- for-each работает с коллекциями и массивами
- Вы можете создать自己的 класс, реализующий Iterable, для использования в for-each
Тест по Iterable, Iterator и for-each
5 вопросов