1. Інтерфейс Iterable
У Java майже всі колекції (окрім Map) реалізують інтерфейс Iterable. Це означає, що їх можна обходити послідовно — елемент за елементом — не вдаючись у деталі внутрішньої структури. Для програміста це виглядає так: «у колекції є вбудований спосіб пройти всі елементи».
Інтерфейс Iterable визначає лише один метод:
Iterator<E> iterator();
Метод iterator() повертає об’єкт типу Iterator — «помічника», який знає, як проходити колекцію крок за кроком. Завдяки цьому працює звичний цикл for-each:
for (ElementType e : collection) {
// ...
}
За лаштунками якраз і ховається той самий Iterator. Додаткова перевага: з його допомогою можна безпечно видаляти елементи під час обходу методом remove(). Якщо намагатися робити це звичайним перебором, легко отримати ConcurrentModificationException.
2. Інтерфейс Iterator
Iterator — це «кур’єр», який уміє рухатися вашою колекцією, не пропускаючи елементи та дотримуючись її порядку обходу.
| Метод | Опис |
|---|---|
|
Чи є ще елементи для обходу? |
|
Повертає наступний елемент і переходить до нього. |
|
Безпечно видаляє поточний елемент. |
Приклад обходу колекції через 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()
}
}
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ