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():
A od Java 8+ wszystko staje się znacznie prostsze: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. 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ść |
|---|---|---|---|---|
|
Java 5+ | - | ❌ | - |
|
Java 5+ | + | ✅ | + |
|
Java 5+ | + | ✅ | ++ |
|
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.
GO TO FULL VERSION