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(); // Видаляємо слова з трьох літер
        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 не буде.

Приклад: видалення рядків коротших за три символи

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.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ