JavaRush /Курси /JAVA 25 SELF /Iterable та Iterator: обхід колекцій

Iterable та Iterator: обхід колекцій

JAVA 25 SELF
Рівень 27 , Лекція 1
Відкрита

1. Інтерфейс Iterable

У Java майже всі колекції (окрім Map) реалізують інтерфейс Iterable. Це означає, що їх можна обходити послідовно — елемент за елементом — не вдаючись у деталі внутрішньої структури. Для програміста це виглядає так: «у колекції є вбудований спосіб пройти всі елементи».

Інтерфейс Iterable визначає лише один метод:

Iterator<E> iterator();

Метод iterator() повертає об’єкт типу Iterator — «помічника», який знає, як проходити колекцію крок за кроком. Завдяки цьому працює звичний цикл for-each:

for (ElementType e : collection) {
    // ...
}

За лаштунками якраз і ховається той самий Iterator. Додаткова перевага: з його допомогою можна безпечно видаляти елементи під час обходу методом remove(). Якщо намагатися робити це звичайним перебором, легко отримати ConcurrentModificationException.

2. Інтерфейс Iterator

Iterator — це «кур’єр», який уміє рухатися вашою колекцією, не пропускаючи елементи та дотримуючись її порядку обходу.

Метод Опис
boolean hasNext()
Чи є ще елементи для обходу?
E next()
Повертає наступний елемент і переходить до нього.
void remove()
Безпечно видаляє поточний елемент.

Приклад обходу колекції через Iterator

import java.util.*;

public class IteratorDemo {
    public static void main(String[] args) {
        List<String> tasks = new ArrayList<>();
        tasks.add("Погладити кота");
        tasks.add("Зробити домашнє завдання");
        tasks.add("Подивитися серіал");

        Iterator<String> it = tasks.iterator();
        while (it.hasNext()) {
            String task = it.next();
            System.out.println("Завдання: " + task);
        }
    }
}

Що тут відбувається?

  • Отримуємо ітератор через tasks.iterator().
  • Поки є наступний елемент (hasNext() повертає true), беремо його через next() і виводимо.
  • Ітератор сам стежить за порядком обходу — вам не потрібно знати, як колекція зберігає елементи всередині.

3. Навіщо потрібен Iterator, якщо є цикли?

За допомогою Iterator можна обходити будь-яку колекцію, навіть без індексів (наприклад, Set). Це універсальний підхід, який не залежить від конкретного типу колекції.

Безпечне видалення елементів

Поширене завдання: пройтися колекцією та видалити деякі елементи. Якщо робити це через for-each, можна отримати помилку:

for (String task : tasks) {
    if (task.contains("кіт")) {
        tasks.remove(task); // БАХ! ConcurrentModificationException
    }
}

Чому так відбувається? Колекція не очікує, що її структуру змінюватимуть напряму під час обходу, ініційованого ітератором.

Правильний спосіб:

Iterator<String> it = tasks.iterator();
while (it.hasNext()) {
    String task = it.next();
    if (task.contains("кіт")) {
        it.remove(); // Усе пройде гладко!
    }
}

Чому не можна просто використовувати індекси?

Тому що не в усіх колекцій є індекси. Наприклад, у HashSet або TreeSet немає поняття «п’ятого елемента». Iterator працює завжди — у цьому його сила.

4. Подробиці про for-each

Поліпшений цикл for (for-each) з’явився ще в Java 5. По суті, це синтаксичний цукор, який дає змогу перебирати елементи максимально просто:

for (String task : tasks) {
    System.out.println("Завдання: " + task);
}

За лаштунками компілятор викликає iterator(), перевіряє елементи через hasNext() і дістає їх за допомогою next(). Читається буквально як фраза природною мовою: «для кожного завдання зі списку».

Коли for-each не підійде?

  • Потрібно видаляти елементи під час обходу (for-each не дає викликати remove() безпосередньо).
  • Потрібен доступ до індексу, щоб, наприклад, замінити елемент за позицією.
  • Ви працюєте з Map — у ній зберігаються пари «ключ–значення», тож для їхнього перебору потрібна окрема логіка.

5. Обхід Map: тонкощі та нюанси

Інтерфейс Map не реалізує Iterable безпосередньо, адже це набір пар «ключ–значення». Втім, Map надає зручні представлення для обходу.

Обхід за ключами

Map<String, String> users = new HashMap<>();
users.put("vasya", "vasya@example.com");
users.put("petya", "petya@gmail.com");

for (String login : users.keySet()) {
    System.out.println("Логін: " + login);
}

Обхід за значеннями

for (String email : users.values()) {
    System.out.println("Email: " + email);
}

Обхід за парами (ключ–значення)

Найуніверсальніший спосіб — обхід через entrySet():

for (Map.Entry<String, String> entry : users.entrySet()) {
    System.out.println("Логін: " + entry.getKey() + ", Email: " + entry.getValue());
}

Цікавий факт: Entry — це внутрішній інтерфейс Map із методами getKey() і getValue(). Так ви одразу отримуєте обидві частини пари.

Обхід через Iterator

Iterator<Map.Entry<String, String>> it = users.entrySet().iterator();
while (it.hasNext()) {
    Map.Entry<String, String> entry = it.next();
    // Можна навіть безпечно видалити елемент:
    if (entry.getKey().startsWith("v")) {
        it.remove();
    }
}

6. Приклади з життя: як обхід колекцій допомагає в застосунку

Приклад: виводимо всі завдання користувача

List<String> tasks = new ArrayList<>();
tasks.add("Зробити домашнє завдання");
tasks.add("Погладити кота");
tasks.add("Подивитися серіал");

System.out.println("Ваші завдання на сьогодні:");
for (String task : tasks) {
    System.out.println("- " + task);
}

Тепер видалимо всі завдання, що містять слово "кіт":

Iterator<String> it = tasks.iterator();
while (it.hasNext()) {
    String task = it.next();
    if (task.contains("кіт")) {
        it.remove();
    }
}
System.out.println("Залишилися завдання:");
for (String task : tasks) {
    System.out.println("- " + task);
}

Приклад: обхід унікальних логінів через Set

Set<String> logins = new HashSet<>();
logins.add("vasya");
logins.add("petya");
logins.add("masha");

for (String login : logins) {
    System.out.println("Користувач: " + login);
}

Зверніть увагу: порядок виведення у Set може бути довільним!

Приклад: обхід Map для відображення користувачів

Map<String, String> users = new HashMap<>();
users.put("vasya", "vasya@example.com");
users.put("petya", "petya@gmail.com");

for (Map.Entry<String, String> entry : users.entrySet()) {
    System.out.println("Логін: " + entry.getKey() + ", Email: " + entry.getValue());
}

7. Iterator.remove(): безпечне видалення елементів

Одна з найчастіших помилок новачків — спроба видалити елементи колекції під час перебору через for-each. Ітератор розв’язує цю проблему за допомогою remove().

Як це працює?

  • Під час виклику it.remove() видаляється поточний елемент — той, який повернув останній next().
  • Це безпечно: колекція не викидає ConcurrentModificationException.

Приклад:

List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6));
Iterator<Integer> it = numbers.iterator();
while (it.hasNext()) {
    int n = it.next();
    if (n % 2 == 0) {
        it.remove(); // Видаляємо всі парні числа
    }
}
System.out.println(numbers); // [1, 3, 5]

Схема обходу колекції

+---------+     +---------+     +---------+
| Element | --> | Element | --> | Element | ...
+---------+     +---------+     +---------+
     ^               ^
     |               |
   next()         next()

Ітератор крокує по елементах, доки hasNext() не поверне false.

8. Типові помилки під час роботи з Iterator та обходом колекцій

Помилка № 1: модифікація колекції під час перебору через for-each.
Спроба видалити елемент прямо всередині for-each призводить до ConcurrentModificationException:

for (String task : tasks) {
    if (task.contains("кіт")) {
        tasks.remove(task); // БАХ! ConcurrentModificationException
    }
}

Використовуйте Iterator і його remove().

Помилка № 2: виклик remove() раніше, ніж next().
Спочатку потрібно отримати поточний елемент через next(), інакше ітератор не розуміє, що саме видаляти.

Iterator<String> it = tasks.iterator();
it.remove(); // Помилка! Спочатку потрібен next()

Помилка № 3: спроба перебирати Map безпосередньо у for-each.
Map не реалізує Iterable напряму — використовуйте keySet(), values() або entrySet().

Map<String, String> users = new HashMap<>();
// for (String entry : users) { ... } // Помилка: так не можна
for (Map.Entry<String, String> e : users.entrySet()) {
    // Правильно:
}

Помилка № 4: зміна колекції поза ітератором під час обходу ітератором.
Під час обходу видаляйте елементи лише через it.remove(), а не через методи колекції.

Iterator<String> it = tasks.iterator();
while (it.hasNext()) {
    String task = it.next();
    if (task.contains("кіт")) {
        tasks.remove(task); // Помилка! Потрібно викликати it.remove()
    }
}
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ