Обход и неизменяемые списки
Гардеробщик проверяет полку с шапками: идёт вдоль неё и смотрит на каждую по очереди. Если в это время кто-то вынет шапку из середины, остальные сдвинутся, и палец гардеробщика окажется не на той шапке. Одну шапку он так и не посмотрит.
Со списком та же беда: менять его посреди обхода опасно. Начнём с обхода, при котором список не меняется. Посчитаем гостей с непустым именем:
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
}
}Давайте разберём программу по строкам:
List.of("", "Anna", "Boris")сразу собирает список из трёх строк. Первая строка пустая: кто-то открыл анкету и ничего не ввёл.for (String g : guests)— тот же цикл for-each, что и у массивов. На каждом кругеgполучает следующий элемент списка.!g.isBlank()пропускает пустую строку, поэтому счётчик доходит до2.
Подойдёт и цикл по индексам: for (int i = 0; i < guests.size(); i++) с чтением guests.get(i). Он нужен, когда важен номер элемента.
Удаление через итератор
Теперь уберём пустые строки из самого списка. Но список из List.of менять нельзя, и почему — разберём в следующем разделе. Поэтому сначала скопируем его в ArrayList.
Безопасно удалять во время обхода умеет итератор. Итератор — объект, который идёт по коллекции элемент за элементом и умеет удалить тот, на котором стоит. Цикл for-each тоже работает через итератор, но спрятанный, и до его метода remove из цикла не добраться. Поэтому итератор берут сами:
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]Разберём по шагам:
new ArrayList<>(guests)создаёт обычный список с теми же элементами.list.iterator()выдаёт итератор. ТипIteratorподключают строкойimport java.util.Iterator;.it.hasNext()отвечает, остались ли элементы, аit.next()возвращает следующий и сдвигает итератор вперёд.it.remove()удаляет элемент, который только что вернулnext(). Итератор сам знает об удалении и не сбивается со счёта.
Как удалить элемент из списка во время обхода — частый вопрос на собеседовании. Ответ: через remove итератора, а не через remove самого списка.
Неизменяемые списки
List.of создаёт неизменяемый список: читать из него можно, а менять нельзя. В гардеробе это витрина под стеклом: шапки видны, но ни взять, ни положить новую не получится.
Методы add, remove и set у такого списка есть, ведь они есть у любого List. Но вызов любого из них, например guests.add("Vera"), останавливает программу:
Exception in thread "main" java.lang.UnsupportedOperationExceptionКомпилятор эту ошибку не ловит. Он видит только тип List, а в нём метод add объявлен. Что список неизменяемый, выясняется лишь при запуске.
Неизменяемые списки удобны для данных, которые менять не должны: дней недели, списка цветов, настроек. Если такой список всё-таки нужно изменить, скопируйте его в new ArrayList<>(…), как в разделе выше.
Частая ошибка
Пустые строки удаляют прямо в цикле for-each:
List<String> list = new ArrayList<>(guests);
for (String g : list) {
if (g.isBlank()) {
list.remove(g); // ✗
}
}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<>(…).