1. Das Problem ConcurrentModificationException
In dieser Vorlesung wird es (fast) nichts Neues geben. Aber sie ist sehr wichtig, denn unsauberes Löschen von Daten gehört zu den folgenschwersten, kaum rückgängig zu machenden Fehlern – insbesondere im Produktivbetrieb. Und eine interessante Klasse, die Sie vermutlich lieben werden, ist doch dabei!
Also ja, noch einmal und immer wieder: kein for-each zum Löschen! So sehr Sie es auch mögen.
Lassen Sie uns mit einem klassischen Beispiel beginnen, das bei vielen Einsteigern (und nicht nur dort) Schmerz und Frust auslöst:
List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6));
// Versuchen wir, alle geraden Zahlen zu entfernen
for (Integer n : numbers) {
if (n % 2 == 0) {
numbers.remove(n); // Boom! ConcurrentModificationException
}
}
Es sieht so aus, als müsste alles funktionieren, aber tatsächlich wirft das Programm eine Ausnahme:
Exception in thread "main" java.util.ConcurrentModificationException
Schauen wir uns genauer an, was hier passiert. Wenn Sie eine Collection mit for-each (oder einem normalen Iterator) durchlaufen, wird intern ein spezieller „Änderungszähler“ geführt. Wenn sich die Collection während der Iteration nicht über den Iterator selbst ändert, erkennt dieser Zähler eine „fremde Änderung“ und wirft eine Ausnahme. Das ist ein Schutzmechanismus, damit das Programm nicht mit einer inkonsistenten Datenstruktur arbeitet.
2. Verwendung von Iterator
Wie löscht man Elemente während der Iteration richtig?
Zur Erinnerung: Ein Iterator ist ein spezielles Objekt, mit dem man eine Collection durchlaufen und Elemente „on the fly“ sicher löschen kann. Er ist wie ein Kellner, der nicht nur die Speisen bringt, sondern während des Rundgangs auch Teller abräumen kann.
Iterator abrufen
Iterator<Integer> it = numbers.iterator();
Iteration mit while und Löschen via it.remove()
So löschen Sie alle geraden Zahlen korrekt aus der Liste:
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(); // Sicheres Löschen des aktuellen Elements
}
}
System.out.println(numbers); // [1, 3, 5]
Wichtig: Elemente dürfen nur über den Iterator selbst (it.remove()) und nur nach einem Aufruf von it.next() gelöscht werden. Wenn Sie remove() zweimal hintereinander ohne next() aufrufen, erhalten Sie eine IllegalStateException.
3. ListIterator: erweiterte Möglichkeiten
Und hier ist die eingangs angekündigte Neuerung! ListIterator ist ein erweiterter Iterator für Listen (List), mit dem man nicht nur löschen, sondern während des Durchlaufs auch Elemente hinzufügen und sich in beide Richtungen bewegen kann (vorwärts und rückwärts).
Unterschied zum normalen Iterator
- Iterator – schlicht: nur vorwärts, nur löschen.
- ListIterator – flexibel und beweglich: vorwärts und rückwärts, löschen, hinzufügen via add(), und das aktuelle Element lässt sich mit set() ersetzen.
Beispiel: Löschen und Hinzufügen von Elementen
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(); // Wir entfernen Wörter mit 3 Buchstaben
it.add("pet"); // Fügen direkt danach "pet" hinter dem entfernten Wort ein
}
}
System.out.println(words); // [pet, pet, bird]
Hinweis: Das Hinzufügen über it.add() fügt das Element unmittelbar nach der aktuellen Iterator-Position ein.
4. Entfernen mit removeIf
Seit Java 8 gibt es die knappe und bequeme Methode removeIf. Sie nimmt ein Lambda (oder ein beliebiges Predicate) entgegen und entfernt alle Elemente, für die die Bedingung true ergibt.
Beispiel: Entfernen aller geraden Zahlen
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]
Das ist nicht nur kürzer, sondern auch sicher: intern verwendet die Methode den richtigen Iterator – es gibt keine ConcurrentModificationException.
Beispiel: Entfernen von Strings kürzer als 3 Zeichen
List<String> words = new ArrayList<>(List.of("hi", "cat", "no", "elephant"));
words.removeIf(word -> word.length() < 3);
System.out.println(words); // [cat, elephant]
Tipp: Wenn Sie Elemente einfach nach einer Bedingung entfernen müssen – verwenden Sie removeIf. Das ist der knappste und modernste Weg.
5. Praktische Empfehlungen
Welche Methode ist vorzuziehen?
- Wenn Sie Elemente nach einer komplexen Bedingung entfernen müssen und Java 8+ verwenden: verwenden Sie removeIf – kurz, verständlich, sicher.
- Wenn Sie auf einer älteren Java-Version sind oder eine komplexere Iterationslogik brauchen: verwenden Sie den Iterator und seine Methode remove().
- Wenn Sie mit List arbeiten und während des Durchlaufs nicht nur löschen, sondern auch hinzufügen möchten: verwenden Sie ListIterator.
Besonderheiten für verschiedene Collection-Typen
- List: unterstützt alle beschriebenen Ansätze (Iterator, ListIterator, removeIf).
- Set: keine Indizes, aber Standard-Iterator und removeIf funktionieren.
- Map: zum Entfernen nach Bedingung den Iterator über entrySet() verwenden:
Und ab Java 8+ wird es deutlich einfacher: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. Praxisbeispiel: Benutzer filtern
Angenommen, wir haben eine Liste von Benutzern und möchten alle Benutzer unter 18 Jahren entfernen.
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("Alice", 17),
new User("Bob", 20),
new User("Victoria", 15),
new User("George", 25)
));
// Minderjährige per removeIf entfernen
users.removeIf(user -> user.age < 18);
System.out.println(users); // [Bob (20), George (25)]
7. Vergleich der Ansätze
Fassen wir das in einer kleinen Tabelle zusammen – das mag das Gehirn.
| Methode | Unterstützt seit Version | Kürze | Sicherheit | Flexibilität |
|---|---|---|---|---|
|
Java 5+ | - | ❌ | - |
|
Java 5+ | + | ✅ | + |
|
Java 5+ | + | ✅ | ++ |
|
Java 8+ | ++ | ✅ | + |
8. Typische Fehler beim Entfernen von Elementen aus Collections
Fehler Nr. 1: Versuch, Elemente in for-each zu löschen
for (String s : list) {
if (s.equals("test")) {
list.remove(s);
}
}
Sie wissen es bereits: So geht es nicht – Sie erhalten eine ConcurrentModificationException! Verwenden Sie einen Iterator oder removeIf.
Fehler Nr. 2: Aufruf von remove() am Iterator ohne next()
Iterator<String> it = list.iterator();
it.remove(); // IllegalStateException - darf nicht vor next() gelöscht werden
Fehler Nr. 3: Versuch, Elemente aus einer unveränderlichen Collection zu entfernen
List<String> immutable = List.of("a", "b", "c");
immutable.removeIf(s -> s.equals("a")); // UnsupportedOperationException
Lösch-Methoden werden für unveränderliche Collections nicht unterstützt.
Fehler Nr. 4: Versuch, Elemente aus Map über values() oder keySet() ohne Iterator zu löschen
for (String key : map.keySet()) {
if (key.startsWith("a")) {
map.remove(key); // ConcurrentModificationException!
}
}
Verwenden Sie den Iterator über entrySet() oder removeIf.
GO TO FULL VERSION