Исправьте цикл, который удаляет элементы из списка во время обхода
Этот фрагмент должен убрать из списка все имена из трёх букв и напечатать остаток. Он компилируется и работает в одном потоке, но во время выполнения падает вместо вывода.
Ограничения:
- оставьте
ArrayList; результат — исходное содержимое минус имена из трёх букв - не заглушайте падение через
try/catch
List<String> names = new ArrayList<>(List.of("ann", "bob", "carol", "dan"));
for (String name : names) {
if (name.length() == 3) {
names.remove(name);
}
}
System.out.println(names);
Найдите и исправьте ошибку.
Цикл for-each работает на собственном fail-fast Iterator списка. names.remove(name) меняет modCount списка за спиной итератора, поэтому следующий next() обнаруживает modCount != expectedModCount и бросает ConcurrentModificationException — второй поток тут ни при чём. Исправление: менять через сам итератор (it.remove() после it.next()) или просто вызвать names.removeIf(n -> n.length() == 3); обход копии тоже работает.
- ✗Считать, что
ConcurrentModificationExceptionбывает только при нескольких потоках - ✗Вызывать
list.remove(...)внутри for-each вместоIterator.remove() - ✗Полагать, что исключение гарантировано — удаление предпоследнего элемента может проскочить
- →Почему удаление предпоследнего элемента иногда вовсе не приводит к исключению?
- →Как итераторы
CopyOnWriteArrayListиConcurrentHashMapизбегают этого падения?
Исправление
List<String> names = new ArrayList<>(List.of("ann", "bob", "carol", "dan"));
// Вариант 1 — удаление через сам итератор
for (Iterator<String> it = names.iterator(); it.hasNext(); ) {
if (it.next().length() == 3) {
it.remove(); // итератор обновляет свой expectedModCount
}
}
// Вариант 2 — самый короткий: removeIf делает то же самое внутри
names.removeIf(n -> n.length() == 3);
// Вариант 3 — обход копии, удаление из оригинала
for (String name : new ArrayList<>(names)) {
if (name.length() == 3) {
names.remove(name);
}
}
System.out.println(names); // [carol]
Что происходило. Цикл for-each — это синтаксический сахар над names.iterator(). ArrayList считает структурные изменения в поле modCount, а его итератор запоминает это значение в expectedModCount при создании. Вызов names.remove(name) идёт мимо итератора и увеличивает modCount; на следующем next() метод checkForComodification() видит расхождение и бросает ConcurrentModificationException.
Это не про многопоточность: одного потока достаточно. Имя у исключения историческое — оно означает «коллекция изменена одновременно с обходом», а не «двумя потоками».
Почему исключение не гарантировано. Проверка происходит в next(), а hasNext() — это всего лишь cursor != size. Если удалить предпоследний элемент, cursor совпадёт с новым size, hasNext() вернёт false, цикл тихо закончится раньше времени — и падения не будет вовсе. Поэтому полагаться на исключение как на сигнал ошибки нельзя.