JavaRush /Курсы /JAVA 25 SELF /Безопасное удаление элементов

Безопасное удаление элементов

JAVA 25 SELF
28 уровень , 2 лекция
Открыта

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():
    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}
    
    А с Java 8+ всё упрощается в разы:
    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. Сравнение подходов

Составим небольшую табличку для закрепления. Наш мозг такое любит.

Способ Поддерживается с версии Краткость Безопасность Гибкость
for-each + remove()
Java 5+ - -
Iterator + remove()
Java 5+ + +
ListIterator
Java 5+ + ++
removeIf
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.

1
Задача
JAVA 25 SELF, 28 уровень, 2 лекция
Недоступна
Переквалификация домашних питомцев в Зоомагазине "Друг" 🐶
Переквалификация домашних питомцев в Зоомагазине "Друг" 🐶
1
Задача
JAVA 25 SELF, 28 уровень, 2 лекция
Недоступна
Чистка списка участников соревнования от "сомнительных" результатов 🏅
Чистка списка участников соревнования от "сомнительных" результатов 🏅
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ