1. Il problema ConcurrentModificationException
In questa lezione non ci sarà (quasi) nulla di nuovo. Ma è molto importante, perché una rimozione disattenta dei dati rientra tra gli errori più irreparabili — soprattutto in produzione. E ci sarà comunque una classe interessante che, con ogni probabilità, amerete!
Quindi sì, ancora e ancora: niente for-each per la rimozione! Per quanto possiate amarlo.
Cominciamo con un esempio classico che provoca dolore e sofferenza a molti principianti (e non solo):
List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6));
// Proviamo a rimuovere tutti i numeri pari
for (Integer n : numbers) {
if (n % 2 == 0) {
numbers.remove(n); // BOOM! ConcurrentModificationException
}
}
Sembra che tutto dovrebbe funzionare, ma in realtà il programma lancia un’eccezione:
Exception in thread "main" java.util.ConcurrentModificationException
Ora analizziamo in dettaglio che cosa è successo. Quando iterate una collezione con for-each (o con un Iterator normale), all’interno della collezione viene mantenuto un «contatore delle modifiche». Se durante l’iterazione la collezione viene modificata non tramite l’iteratore stesso, questo contatore rileva un «intervento esterno» e lancia un’eccezione. È una protezione dagli errori, per evitare che il programma lavori con una struttura dati danneggiata.
2. Uso di Iterator
Come rimuovere correttamente gli elementi durante l’iterazione?
Ricordo: Iterator è un oggetto speciale che permette di scorrere una collezione e di rimuovere in sicurezza gli elementi «al volo». È come un cameriere che non solo serve i piatti, ma può anche togliere un piatto proprio mentre gira intorno al tavolo.
Ottenere un iteratore
Iterator<Integer> it = numbers.iterator();
Iterazione con while e rimozione tramite it.remove()
Ecco il modo corretto per rimuovere tutti i numeri pari da una lista:
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(); // Rimozione sicura dell'elemento corrente
}
}
System.out.println(numbers); // [1, 3, 5]
Punto importante: gli elementi possono essere rimossi solo tramite l’iteratore stesso (it.remove()) e solo dopo aver chiamato it.next(). Se provate a chiamare remove() due volte di seguito senza next(), otterrete IllegalStateException.
3. ListIterator: funzionalità avanzate
Ed ecco la novità annunciata all’inizio della lezione! ListIterator è un iteratore «potenziato» per le liste (List) che consente non solo di rimuovere, ma anche di aggiungere elementi durante la scansione, e inoltre di muoversi in entrambe le direzioni (avanti e indietro).
Differenze rispetto a un Iterator normale
- Iterator — lineare e inflessibile: solo avanti, solo rimozione.
- ListIterator — flessibile e dinamico: avanti e indietro, rimozione, aggiunta tramite add(), e si può anche sostituire l’elemento corrente con il metodo set().
Esempio: rimozione e aggiunta di elementi
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(); // Rimuoviamo le parole di 3 lettere
it.add("pet"); // E subito aggiungiamo "pet" dopo la parola rimossa
}
}
System.out.println(words); // [pet, pet, bird]
Nota: l’inserimento tramite it.add() inserisce l’elemento subito dopo la posizione corrente dell’iteratore.
4. Rimozione tramite removeIf
A partire da Java 8 è arrivato un metodo conciso e comodo: removeIf. Accetta una lambda (o qualsiasi Predicate) e rimuove tutti gli elementi per cui la condizione restituisce true.
Esempio: rimozione di tutti i numeri pari
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]
Non è solo più breve, ma anche sicuro: internamente il metodo usa l’iteratore corretto — nessuna ConcurrentModificationException.
Esempio: rimozione delle stringhe più corte di 3 caratteri
List<String> words = new ArrayList<>(List.of("hi", "cat", "no", "elephant"));
words.removeIf(word -> word.length() < 3);
System.out.println(words); // [cat, elephant]
Consiglio: se dovete semplicemente rimuovere elementi in base a una condizione — utilizzate removeIf. È il modo più conciso e moderno.
5. Raccomandazioni pratiche
Quale approccio è preferibile?
- Se dovete rimuovere elementi in base a una condizione anche complessa e usate Java 8+: usate removeIf — breve, chiaro, sicuro.
- Se siete su una versione precedente di Java o vi serve una logica di iterazione più articolata: usate Iterator e il suo metodo remove().
- Se lavorate con List e volete non solo rimuovere ma anche aggiungere elementi durante la scansione: usate ListIterator.
Particolarità per diversi tipi di collezioni
- List: supporta tutti gli approcci descritti (Iterator, ListIterator, removeIf).
- Set: non ha indici, ma i classici Iterator e removeIf funzionano.
- Map: per rimuovere in base a una condizione usate un iteratore su entrySet():
Con Java 8+ tutto si semplifica di molto: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. Esempio pratico: filtrare gli utenti
Supponiamo di avere un elenco di utenti e vogliamo rimuovere tutti gli utenti con meno di 18 anni.
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)
));
// Rimuoviamo i minorenni con removeIf
users.removeIf(user -> user.age < 18);
System.out.println(users); // [Boris (20), Gleb (25)]
7. Confronto degli approcci
Facciamo una piccola tabella per fissare le idee. Al nostro cervello piace.
| Metodo | Supportato dalla versione | Sintesi | Sicurezza | Flessibilità |
|---|---|---|---|---|
|
Java 5+ | - | ❌ | - |
|
Java 5+ | + | ✅ | + |
|
Java 5+ | + | ✅ | ++ |
|
Java 8+ | ++ | ✅ | + |
8. Errori tipici durante la rimozione di elementi dalle collezioni
Errore n. 1: tentare di rimuovere elementi in for-each
for (String s : list) {
if (s.equals("test")) {
list.remove(s);
}
}
Lo sapete già: non si fa — otterrete ConcurrentModificationException! Usate un iteratore o removeIf.
Errore n. 2: chiamare remove() sull’iteratore senza next()
Iterator<String> it = list.iterator();
it.remove(); // IllegalStateException — non si può rimuovere prima di chiamare next()
Errore n. 3: tentare di rimuovere elementi da una collezione non modificabile
List<String> immutable = List.of("a", "b", "c");
immutable.removeIf(s -> s.equals("a")); // UnsupportedOperationException
I metodi di rimozione non sono supportati per le collezioni immutabili.
Errore n. 4: tentare di rimuovere elementi da Map tramite values() o keySet() senza iteratore
for (String key : map.keySet()) {
if (key.startsWith("a")) {
map.remove(key); // ConcurrentModificationException!
}
}
Usate un iteratore su entrySet() o removeIf.
GO TO FULL VERSION