1. Передісторія появи ітератора

Ви вже познайомилися із множиною HashSet. І якщо ви справді розбиралися з нею, а не просто переглядали лекцію, то мали поставити собі запитання:

Як вивести на екран список усіх елементів HashSet? Адже у множини немає методів get() і set()!

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

Свого часу програмісти винайшли багато складних структур даних, як-от граф, дерево або, скажімо, список списків.

Багато контейнерів змінюють порядок своїх елементів під час додавання нових або видалення наявних елементів. Наприклад, є список, в якому зберігаються відсортовані елементи, але новий елемент практично завжди вставляється в середину списку.

От ми й отримуємо ситуацію, коли є контейнер з елементами, елементи, власне, теж є, а чіткого порядку немає.

Припустімо, ми хочемо скопіювати всі елементи з такої колекції в масив або список. Нам потрібно отримати всі елементи, і нам усе одно, в якому порядку ми їх обійдемо, головне — не повторюватися. Як нам це зробити?


2. Ітератор у колекції

Для вирішення описаної вище проблеми було запропоновано використовувати ітератор.

Ітератор — це спеціальний об'єкт у колекції, який дає змогу не повторюючись обійти всі елементи колекції.

Щоб отримати ітератор для колекції, використовують такий код:

Iterator<Тип> it = ім'я.iterator();

де ім'я — це ім'я змінної-колекції, Тип — це тип елементів колекції, iterator() — це метод колекції, it — це ім'я змінної-об'єкта-ітератора.

Об'єкт-ітератор має 3 методи:

Метод Опис
Тип next()
Повертає черговий елемент колекції
boolean hasNext()
Перевіряє, чи залишилися непройдені елементи
void remove()
Видаляє поточний елемент колекції

Ці методи дещо подібні до методів класу Scanner: nextInt() і hasNextInt().

Метод next() повертає черговий елемент колекції, для якої було отримано ітератор.

Метод hasNext() перевіряє, чи залишилися в колекції елементи, які ітератор ще не повернув.

За допомогою цього коду можна вивести на екран усі елементи множини HashSet:

Код Примітки
HashSet<String> set = new HashSet<String>();

set.add("Привіт");
set.add("Hello");
set.add("Hola");
set.add("Bonjour");
set.add("Ciao");
set.add("Namaste");

Iterator<String> it = set.iterator();
while (it.hasNext())
{
   String str = it.next();
   System.out.println(str);
}
Створюємо об'єкт типу HashSet, який зберігає елементи типу String.


Записуємо в set привітання різними мовами.




Отримуємо об'єкт-ітератор для множини set.
Поки ще є непройдені елементи

Отримуємо наступний елемент
Виводимо елемент на екран


3. Цикл for-each

Основний недолік ітератора полягає в тому, що з його використанням ми отримуємо ще більш громіздкий код, ніж з використанням циклу for.

Порівняймо коди для виведення списку на екран, створені з використанням циклу for і з використанням ітератора:

Ітератор Цикл for
ArrayList<String> list = new ArrayList<String>();

Iterator<String> it = list.iterator();
while (it.hasNext())
{
   String str = it.next();
   System.out.println(str);
}
ArrayList<String> list = new ArrayList<String>();

for (int i = 0; i < list.size(); i++)
{
   String str = list.get(i);
   System.out.println(str);
}

Справді, обходити елементи списку ArrayList краще за допомогою циклу — код значно коротший.

Однак розробники Java знову вирішили підсипати нам трохи синтаксичного цукру.

Вони додали в Java новий тип циклів — for-each. Загальний формат цього циклу такий:

for(Тип ім'я:колекція)

де колекція — це ім'я змінної-колекції, Тип — це тип елементів колекції, а ім'я — це ім'я змінної, яка на кожному витку циклу набуває чергового значення з колекції.

Цей цикл обходить усі елементи колекції за допомогою прихованого ітератора. Отак він працює:

Цикл for-each Що бачить компілятор: цикл з ітератором
ArrayList<String> list = new ArrayList<String>();

for (String str: list)
{
   System.out.println(str);
}
ArrayList<String> list = new ArrayList<String>();
Iterator<String> it = list.iterator();

while (it.hasNext())
{
   String str = it.next();
   System.out.println(str);
}

Коли компілятор зустріне у вашому коді цикл for-each, він просто замінить його на код, наведений праворуч: додасть метод отримання ітератора та всі відсутні виклики методів.

Програмісти дуже люблять цикл for-each і практично завжди використовують його, коли потрібно обійти всі елементи колекції.

З використанням циклу for-each навіть обхід списку ArrayList буде коротшим:

Цикл for-each Цикл for
ArrayList<String> list = new ArrayList<String>();

for (String str: list)
{
   System.out.println(str);
}
ArrayList<String> list = new ArrayList<String>();

for (int i = 0; i < list.size(); i++)
{
   String str = list.get(i);
   System.out.println(str);
}


4. Видалення елемента в циклі for-each

Цикл for-each має один недолік: він не вміє правильно видаляти елементи. Якщо ви напишете такий код, отримаєте помилку.

Код Примітка
ArrayList<String> list = new ArrayList<String>();

list.add("Привіт");
list.add("Hello");
list.add("Hola");
list.add("Bonjour");
list.add("Ciao");
list.add("Namaste");

for (String str: list)
{
   if (str.equals("Hello"))
      list.remove(str);
}












Під час видалення станеться помилка!

Це дуже гарний і зрозумілий код, але працювати він не буде.

Важливо!

Не можна змінювати колекцію, поки ви обходите її за допомогою ітератора.

Є три способи подолати це обмеження.

1 Використання іншого циклу

Якщо ви обходите колекцію ArrayList, можна скористатися звичайним циклом із лічильником i.

Код
for (int i = 0; i < list.size(); i++)
{
   String str = list.get(i);

   if (str.equals("Hello"))
   {
      list.remove(str);
      i--;    // потрібно зменшити i, тому що після видалення елементи зсунулися
   }
}

Однак цей варіант не підходить для колекцій HashSet і HashMap.

2 Явне використання ітератора

Можна в явний спосіб скористатися ітератором і його методом remove().

Працюючий варіант Непрацюючий варіант
Iterator<String> it = set.iterator();
while (it.hasNext())
{
   String str = it.next();
   if (str.equals("Hello"))
       it.remove();
}

for (String str: list) { if (str.equals("Hello")) list.remove(str); }

Зверніть увагу, що метод remove() ми викликаємо для об'єкта-ітератора! Ітератор «знатиме» про видалення елемента і зможе правильно обробити цю ситуацію.

3 Використання копії колекції

Можна також створити копію колекції та використовувати її в циклі for-each, а видаляти елементи з колекції-оригіналу.

Код Примітка
ArrayList<String> listCopy = new ArrayList(list);

for (String str: listCopy)
{
   if (str.equals("Hello"))
      list.remove(str);
}
Копію колекції створити дуже легко



У циклі використовується ітератор колекції-копії.
Елементи видаляються з колекції list.

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