CodeGym /Kursy /JAVA 25 SELF /Bezpieczne usuwanie elementów

Bezpieczne usuwanie elementów

JAVA 25 SELF
Poziom 28 , Lekcja 2
Dostępny

1. Problem ConcurrentModificationException

W tym wykładzie nie będzie (prawie) nic nowego. Ale jest on bardzo ważny, ponieważ nieostrożne usuwanie danych należy do jednych z najbardziej nieodwracalnych błędów — zwłaszcza na produkcji. I będzie też jedna ciekawa klasa, którą najpewniej polubicie!

Tak więc, jeszcze raz i jeszcze raz: żadnego for-each do usuwania! Niezależnie od tego, jak bardzo go lubicie.

Zacznijmy od klasycznego przykładu, który powoduje u wielu początkujących (i nie tylko) ból i cierpienie:

List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6));

// Spróbujmy usunąć wszystkie liczby parzyste
for (Integer n : numbers) {
    if (n % 2 == 0) {
        numbers.remove(n); // BUM! ConcurrentModificationException
    }
}

Wygląda tak, jakby wszystko powinno działać, ale w rzeczywistości program wyrzuca wyjątek:

Exception in thread "main" java.util.ConcurrentModificationException

Przyjrzyjmy się teraz dokładniej, co tu zaszło. Gdy iterujesz po kolekcji za pomocą for-each (lub zwykłego Iterator), wewnątrz kolekcji utrzymywany jest specjalny „„licznik zmian””. Jeśli podczas iteracji kolekcja zmienia się nie przez sam iterator, ten licznik wykrywa „„obcą ingerencję”” i rzuca wyjątek. To zabezpieczenie przed błędami, aby program nie pracował na uszkodzonej strukturze danych.

2. Użycie Iterator

Jak poprawnie usuwać elementy podczas iteracji?

Przypomnę: Iterator to specjalny obiekt, który pozwala iterować po kolekcji i bezpiecznie usuwać elementy „w locie”. Jest jak kelner, który nie tylko roznosi dania, ale może też zabrać talerz podczas obchodzenia stołu.

Pobranie iteratora

Iterator<Integer> it = numbers.iterator();

Iteracja za pomocą while i usuwanie przez it.remove()

Oto poprawny sposób usunięcia wszystkich liczb parzystych z listy:

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(); // Bezpieczne usunięcie bieżącego elementu
    }
}
System.out.println(numbers); // [1, 3, 5]

Ważna uwaga: elementy można usuwać tylko przez sam iterator (it.remove()) i tylko po wywołaniu it.next(). Jeśli spróbujesz wywołać remove() dwukrotnie z rzędu bez next(), dostaniesz IllegalStateException.

3. ListIterator: rozszerzone możliwości

I oto zapowiedziana na początku wykładu nowość! ListIterator to „„podrasowany”” iterator dla list (List), który pozwala nie tylko usuwać, ale i dodawać elementy w trakcie przechodzenia, a także poruszać się w obu kierunkach (do przodu i do tyłu).

Różnice względem zwykłego Iterator

  • Iterator — prostolinijny i nieustępliwy: tylko do przodu, tylko usuwanie.
  • ListIterator — elastyczny i zwrotny: do przodu i do tyłu, usuwanie, dodawanie przez add(), a do tego można podmienić bieżący element metodą set().

Przykład: usuwanie i dodawanie elementów

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(); // Usuwamy słowa o długości 3 liter
        it.add("pet"); // Od razu dodajemy "pet" po usuniętym słowie
    }
}
System.out.println(words); // [pet, pet, bird]

Uwaga: dodanie przez it.add() wstawia element od razu po bieżącej pozycji iteratora.

4. Usuwanie za pomocą removeIf

Od Java 8 pojawiła się zwięzła i wygodna metoda removeIf. Przyjmuje wyrażenie lambda (lub dowolny Predicate) i usuwa wszystkie elementy, dla których warunek zwraca true.

Przykład: usuwanie wszystkich liczb parzystych

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]

To nie tylko krótsze, ale i bezpieczne: wewnątrz metoda używa właściwego iteratora — nie będzie żadnego ConcurrentModificationException.

Przykład: usuwanie napisów krótszych niż 3 znaki

List<String> words = new ArrayList<>(List.of("hi", "cat", "no", "elephant"));

words.removeIf(word -> word.length() < 3);

System.out.println(words); // [cat, elephant]

Wskazówka: jeśli musisz po prostu usunąć elementy według warunku — użyj removeIf. To najzwięźlejszy i nowoczesny sposób.

5. Praktyczne zalecenia

Który sposób wybrać?

  • Jeśli trzeba usunąć elementy według złożonego warunku i używasz Java 8+: użyj removeIf — krótko, jasno, bezpiecznie.
  • Jeśli pracujesz na starszej wersji Javy lub potrzebujesz bardziej złożonej logiki iteracji: użyj Iterator i jego metody remove().
  • Jeśli pracujesz z List i chcesz nie tylko usuwać, ale też dodawać elementy w trakcie przechodzenia: użyj ListIterator.

Specyfika dla różnych typów kolekcji

  • List: obsługuje wszystkie opisane podejścia (Iterator, ListIterator, removeIf).
  • Set: nie ma indeksów, ale standardowe Iterator i removeIf działają.
  • Map: do usuwania według warunku użyj iteratora po 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}
    
    A od Java 8+ wszystko staje się znacznie prostsze:
    map.entrySet().removeIf(entry -> entry.getValue() % 2 == 0);
    

6. Przykład z praktyki: filtrowanie użytkowników

Załóżmy, że mamy listę użytkowników i chcemy usunąć wszystkich, którzy mają mniej niż 18 lat.

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("Anya", 17),
    new User("Boris", 20),
    new User("Vika", 15),
    new User("Gleb", 25)
));

// Usuwamy niepełnoletnich przez removeIf
users.removeIf(user -> user.age < 18);

System.out.println(users); // [Boris (20), Gleb (25)]

7. Porównanie podejść

Zróbmy małą tabelkę dla utrwalenia. Nasz mózg to lubi.

Sposób Wspierany od wersji Zwięzłość Bezpieczeństwo Elastyczność
for-each + remove()
Java 5+ - -
Iterator + remove()
Java 5+ + +
ListIterator
Java 5+ + ++
removeIf
Java 8+ ++ +

8. Typowe błędy przy usuwaniu elementów z kolekcji

Błąd nr 1: próba usuwania elementów w for-each

for (String s : list) {
    if (s.equals("test")) {
        list.remove(s); 
    }
}

Już wiesz: tak robić nie wolno — skończy się to ConcurrentModificationException! Użyj iteratora albo removeIf.

Błąd nr 2: wywołanie remove() na iteratorze bez next()

Iterator<String> it = list.iterator();
it.remove(); // IllegalStateException — nie można usuwać przed wywołaniem next()

Błąd nr 3: próba usuwania elementów z kolekcji, której nie można modyfikować

List<String> immutable = List.of("a", "b", "c");
immutable.removeIf(s -> s.equals("a")); // UnsupportedOperationException

Metody usuwania nie są obsługiwane dla niemodyfikowalnych kolekcji.

Błąd nr 4: próba usuwania elementów z Map przez values() lub keySet() bez iteratora

for (String key : map.keySet()) {
    if (key.startsWith("a")) {
        map.remove(key); // ConcurrentModificationException!
    }
}

Używaj iteratora po entrySet() albo removeIf.

Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION