Перейти к содержимому
Теория · Коллекции и обобщения · урок 5 из 13

Обход и неизменяемые списки

Гардеробщик проверяет полку с шапками: идёт вдоль неё и смотрит на каждую по очереди. Если в это время кто-то вынет шапку из середины, остальные сдвинутся, и палец гардеробщика окажется не на той шапке. Одну шапку он так и не посмотрит.

Со списком та же беда: менять его посреди обхода опасно. Начнём с обхода, при котором список не меняется. Посчитаем гостей с непустым именем:

JAVA
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> guests = List.of("", "Anna", "Boris");
        int count = 0;
        for (String g : guests) {
            if (!g.isBlank()) {
                count++;
            }
        }
        System.out.println(count); // 2
    }
}

Давайте разберём программу по строкам:

  1. List.of("", "Anna", "Boris") сразу собирает список из трёх строк. Первая строка пустая: кто-то открыл анкету и ничего не ввёл.
  2. for (String g : guests) — тот же цикл for-each, что и у массивов. На каждом круге g получает следующий элемент списка.
  3. !g.isBlank() пропускает пустую строку, поэтому счётчик доходит до 2.

Подойдёт и цикл по индексам: for (int i = 0; i < guests.size(); i++) с чтением guests.get(i). Он нужен, когда важен номер элемента.

Удаление через итератор

Теперь уберём пустые строки из самого списка. Но список из List.of менять нельзя, и почему — разберём в следующем разделе. Поэтому сначала скопируем его в ArrayList.

Безопасно удалять во время обхода умеет итератор. Итератор — объект, который идёт по коллекции элемент за элементом и умеет удалить тот, на котором стоит. Цикл for-each тоже работает через итератор, но спрятанный, и до его метода remove из цикла не добраться. Поэтому итератор берут сами:

JAVA
List<String> list = new ArrayList<>(guests);
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    if (it.next().isBlank()) {
        it.remove();
    }
}
System.out.println(list); // [Anna, Boris]

Разберём по шагам:

  1. new ArrayList<>(guests) создаёт обычный список с теми же элементами.
  2. list.iterator() выдаёт итератор. Тип Iterator подключают строкой import java.util.Iterator;.
  3. it.hasNext() отвечает, остались ли элементы, а it.next() возвращает следующий и сдвигает итератор вперёд.
  4. it.remove() удаляет элемент, который только что вернул next(). Итератор сам знает об удалении и не сбивается со счёта.

Как удалить элемент из списка во время обхода — частый вопрос на собеседовании. Ответ: через remove итератора, а не через remove самого списка.

Неизменяемые списки

List.of создаёт неизменяемый список: читать из него можно, а менять нельзя. В гардеробе это витрина под стеклом: шапки видны, но ни взять, ни положить новую не получится.

Методы add, remove и set у такого списка есть, ведь они есть у любого List. Но вызов любого из них, например guests.add("Vera"), останавливает программу:

TEXT
Exception in thread "main" java.lang.UnsupportedOperationException

Компилятор эту ошибку не ловит. Он видит только тип List, а в нём метод add объявлен. Что список неизменяемый, выясняется лишь при запуске.

Неизменяемые списки удобны для данных, которые менять не должны: дней недели, списка цветов, настроек. Если такой список всё-таки нужно изменить, скопируйте его в new ArrayList<>(…), как в разделе выше.

Частая ошибка

Пустые строки удаляют прямо в цикле for-each:

JAVA
List<String> list = new ArrayList<>(guests);
for (String g : list) {
    if (g.isBlank()) {
        list.remove(g); // ✗
    }
}
TEXT
Exception in thread "main" java.util.ConcurrentModificationException

Пустую строку список удалил, но на следующем круге спрятанный итератор заметил, что список изменили в обход него, и бросил исключение. Это защита от беды с полкой: элементы сдвинулись, и обход пошёл бы не туда.

Здесь картинка с гардеробщиком неточна: Java обычно замечает сдвиг сама. Но не всегда. Если в таком цикле удалить предпоследний элемент, цикл закончится раньше и последний элемент не проверит. Поэтому во время обхода удаляйте только через it.remove().

Главное

  • Список обходят циклом for-each или циклом по индексам с get(i).
  • Во время обхода элементы удаляют через итератор: it.next() берёт элемент, it.remove() его удаляет.
  • list.remove(…) внутри for-each обычно бросает ConcurrentModificationException, а иногда молча пропускает элемент.
  • List.of создаёт неизменяемый список: add, remove и set бросают UnsupportedOperationException, а изменяемую копию даёт new ArrayList<>(…).