1. Проблема ConcurrentModificationException
В этой лекции не будет (почти) ничего нового. Но она очень важна, поскольку неаккуратное удаление данных относится к одним из самых непоправимых ошибок — особенно в продакшене. И один интересный класс, который вы, скорее всего, полюбите, всё-таки будет!
Так что да, ещё раз и ещё раз: никакого for-each для удаления! Как бы вы его ни любили.
Давайте начнем с классического примера, который вызывает у многих новичков (и не только) боль и страдания:
List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6));
// Попробуем удалить все чётные числа
for (Integer n : numbers) {
if (n % 2 == 0) {
numbers.remove(n); // БУМ! ConcurrentModificationException
}
}
Выглядит так, будто всё должно работать, но на деле программа выбрасывает исключение:
Exception in thread "main" java.util.ConcurrentModificationException
Теперь давайте подробнее разберёмся, что тут произошло. Когда вы перебираете коллекцию с помощью for-each (или обычного Iterator), внутри коллекции поддерживается специальный «счётчик изменений». Если во время перебора коллекция меняется не через сам итератор, этот счётчик обнаруживает «постороннее вмешательство» и бросает исключение. Это защита от ошибок, чтобы программа не работала с повреждённой структурой данных.
2. Использование Iterator
Как правильно удалять элементы во время перебора?
Напомню: Iterator — специальный объект, который позволяет перебирать коллекцию и безопасно удалять элементы «на лету». Он как официант, который не только разносит блюда, но и может убрать тарелку прямо во время обхода стола.
Получение итератора
Iterator<Integer> it = numbers.iterator();
Перебор с помощью while и удаление через it.remove()
Вот правильный способ удалить все чётные числа из списка:
List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6));
Iterator<Integer> it = numbers.iterator();
while (it.hasNext()) {
Integer n = it.next();
if (n % 2 == 0) {
it.remove(); // Безопасное удаление текущего элемента
}
}
System.out.println(numbers); // [1, 3, 5]
Важный момент: удалять элементы можно только через сам итератор (it.remove()) и только после вызова it.next(). Если попытаться вызвать remove() дважды подряд без next(), получите IllegalStateException.
3. ListIterator: расширенные возможности
А вот и анонсированная в начале лекции новинка! ListIterator — это «прокачанный» итератор для списков (List), который позволяет не только удалять, но и добавлять элементы во время обхода, а также двигаться в обе стороны (вперёд и назад).
Отличие от обычного Iterator
- Iterator — прямолинеен и неотступен: только вперёд, только удаление.
- ListIterator — гибок и подвижен: вперёд и назад, удаление, добавление через add(), а ещё можно заменить текущий элемент методом set().
Пример: удаление и добавление элементов
List<String> words = new ArrayList<>(List.of("cat", "dog", "bird"));
ListIterator<String> it = words.listIterator();
while (it.hasNext()) {
String word = it.next();
if (word.length() == 3) {
it.remove(); // Удаляем слова из 3 букв
it.add("pet"); // Тут же добавляем "pet" после удалённого слова
}
}
System.out.println(words); // [pet, pet, bird]
Замечание: добавление через it.add() вставляет элемент сразу после текущей позиции итератора.
4. Удаление с помощью removeIf
Начиная с Java 8, появился лаконичный и удобный метод removeIf. Он принимает лямбда-выражение (или любой Predicate) и удаляет все элементы, для которых условие возвращает true.
Пример: удаление всех чётных чисел
List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6));
numbers.removeIf(n -> n % 2 == 0);
System.out.println(numbers); // [1, 3, 5]
Это не только короче, но и безопасно: внутри метод использует правильный итератор — никакого ConcurrentModificationException не будет.
Пример: удаление строк короче 3 символов
List<String> words = new ArrayList<>(List.of("hi", "cat", "no", "elephant"));
words.removeIf(word -> word.length() < 3);
System.out.println(words); // [cat, elephant]
Совет: если вам нужно просто удалить элементы по условию — используйте removeIf. Это самый лаконичный и современный способ.
5. Практические рекомендации
Какой способ предпочтительнее?
- Если нужно удалить элементы по сложному условию и вы используете Java 8+: используйте removeIf — коротко, понятно, безопасно.
- Если вы находитесь в старой версии Java или нужна более сложная логика перебора: используйте Iterator и его метод remove().
- Если работаете с List и хотите не только удалять, но и добавлять элементы во время обхода: используйте ListIterator.
Особенности для разных типов коллекций
- List: поддерживает все описанные подходы (Iterator, ListIterator, removeIf).
- Set: индексов нет, но стандартные Iterator и removeIf работают.
- Map: для удаления по условию используйте итератор по entrySet():
А с Java 8+ всё упрощается в разы:Map<String, Integer> map = new HashMap<>(Map.of("a", 1, "b", 2, "c", 3)); Iterator<Map.Entry<String, Integer>> it = map.entrySet().iterator(); while (it.hasNext()) { Map.Entry<String, Integer> entry = it.next(); if (entry.getValue() % 2 == 0) { it.remove(); } } System.out.println(map); // {a=1, c=3}map.entrySet().removeIf(entry -> entry.getValue() % 2 == 0);
6. Пример из практики: фильтрация пользователей
Пусть у нас есть список пользователей, и мы хотим удалить всех пользователей младше 18 лет.
class User {
String name;
int age;
User(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return name + " (" + age + ")";
}
}
List<User> users = new ArrayList<>(List.of(
new User("Аня", 17),
new User("Борис", 20),
new User("Вика", 15),
new User("Глеб", 25)
));
// Удаляем несовершеннолетних через removeIf
users.removeIf(user -> user.age < 18);
System.out.println(users); // [Борис (20), Глеб (25)]
7. Сравнение подходов
Составим небольшую табличку для закрепления. Наш мозг такое любит.
| Способ | Поддерживается с версии | Краткость | Безопасность | Гибкость |
|---|---|---|---|---|
|
Java 5+ | - | ❌ | - |
|
Java 5+ | + | ✅ | + |
|
Java 5+ | + | ✅ | ++ |
|
Java 8+ | ++ | ✅ | + |
8. Типичные ошибки при удалении элементов из коллекций
Ошибка №1: попытка удалить элементы в for-each
for (String s : list) {
if (s.equals("test")) {
list.remove(s);
}
}
Вы уже в курсе: так делать нельзя — получите ConcurrentModificationException! Используйте итератор или removeIf.
Ошибка №2: вызов remove() у итератора без next()
Iterator<String> it = list.iterator();
it.remove(); // IllegalStateException — нельзя удалить до вызова next()
Ошибка №3: попытка удалить элементы из коллекции, которую нельзя изменять
List<String> immutable = List.of("a", "b", "c");
immutable.removeIf(s -> s.equals("a")); // UnsupportedOperationException
Методы удаления не поддерживаются для неизменяемых коллекций.
Ошибка №4: попытка удаления элементов из Map через values() или keySet() без итератора
for (String key : map.keySet()) {
if (key.startsWith("a")) {
map.remove(key); // ConcurrentModificationException!
}
}
Используйте итератор по entrySet() или removeIf.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ