Ein altbekanntes Problem. Man iteriert in einer For-Each-Schleife über das KeySet einer Map und möchte einzelne Elemente aus der Map über eine eigene Methode löschen:
private Map keyValueMap = new HashMap<>();
public void removeAll() {
for (String key : keyValueMap.keySet()) {
if (key == something) {
remove(key);
}
}
}
public void remove(String key) {
// .. do some stuff here
keyValueMap.remove(key);
// .. do some more stuff here
}
Belohnt wird man prompt mit dieser Exception:
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.HashMap$HashIterator.nextEntry(HashMap.java:806)
at java.util.HashMap$KeyIterator.next(HashMap.java:841)
Der Grund ist recht simpel: KeySet und die Entries der Map sind miteinander synchronisiert. Löscht man aus der Map einen Eintrag heraus, ändert sich auch sofort das KeySet und das führt zur bekannten Exception.
Lösungen dieses Problems bedienen sich in den meisten Fällen eines Iterators:
public void removeAll() {
Iterator it = keyValueMap.keySet().iterator();
while (it.hasNext()) {
String key = it.next();
if (key == something) {
it.remove();
}
}
}
Das hat allerdings den Nachteil, dass man zum Löschen auf den Iterator Zugriff haben muss. In obigem Beispiel würde das aber ohne größere Anpassungen nicht funktionieren. Außerdem ist das Iterator-Konstrukt doch umständlicher und unübersichtlicher.
Eine weitaus intuitivere Variante ist diese hier:
public void removeAll() {
for (String key : new HashSet<>(keyValueMap.keySet())) {
if (key == something) {
remove(key);
}
}
}
Die Unterschiede zur ursprünglichen Variante sind marginal. Es wird lediglich das KeySet in einem neuen HashSet gewrapped. Damit greift man in der For-Each-Schleife nun auf eine unsynchronisierte Kopie des KeySets zu und umgeht damit die ConcurrentModificationException. Nachteil ist natürlich, dass man für die Kopie des KeySets neuen Speicher benötigt. Besonders für große Maps muss man sich dessen bewusst sein.