Iteratori Java
Scorri le collezioni Java con l'interfaccia Iterator — hasNext, next, remove — e il contratto Iterable.
Ogni volta che scrivi for (T x : collection) in Java, stai invocando una coppia nascosta: l'interfaccia Iterable<T> che consente di iterare sulla collezione, e l'Iterator<T> che essa fornisce al ciclo. Il ciclo for-each è zucchero sintattico; l'Iterator è il motore. Capire cosa fa — e cosa possono lanciare i suoi tre metodi — è la differenza tra "il mio attraversamento della lista funziona la maggior parte delle volte" e "so esattamente quando si romperà."
Questo capitolo riguarda il semplice Iterator<E>. Il più ricco ListIterator<E>, che può scorrere all'indietro e modificare durante l'iterazione, ha il suo capitolo dedicato subito dopo.
Le due interfacce
Iterable<T> è il contratto per "cose su cui puoi iterare":
public interface Iterable<T> {
Iterator<T> iterator();
default void forEach(Consumer<? super T> action) { ... }
default Spliterator<T> spliterator() { ... }
}Iterator<T> è il cursore:
public interface Iterator<E> {
boolean hasNext();
E next();
default void remove() { throw new UnsupportedOperationException(); }
default void forEachRemaining(Consumer<? super E> action) { ... }
}Ogni Collection<E> estende Iterable<E> — ecco perché un ciclo for-each funziona su una List, un Set, una Queue. (Una Map non è Iterable; si itera sul suo entrySet(), keySet(), o values() — vedi come iterare una HashMap.) Il ciclo for-each:
for (String name : names) { System.out.println(name); }viene compilato in:
for (Iterator<String> it = names.iterator(); it.hasNext(); ) {
String name = it.next();
System.out.println(name);
}Una volta vista questa trasformazione, le altre regole dell'Iterator hanno senso.
I tre metodi e cosa lanciano
hasNext() restituisce true se next() avrebbe successo. È idempotente — chiamarlo due volte di fila è sicuro. Non lancia mai eccezioni (nelle implementazioni ben comportate).
next() avanza il cursore e restituisce l'elemento. Lancia NoSuchElementException se non c'è un elemento successivo. Questo è l'unico metodo dell'iteratore che lancia un'eccezione per design quando viene usato in modo errato. Proteggi sempre con hasNext() se c'è qualche possibilità che la collezione sia vuota:
while (it.hasNext()) { use(it.next()); } // safe patternremove() rimuove l'elemento restituito più di recente da next(). È un metodo default che lancia UnsupportedOperationException a meno che l'iteratore non lo implementi. ArrayList, HashMap.keySet().iterator() e simili lo supportano tutti. Gli iteratori restituiti da List.of(...), Collections.unmodifiableList(...) e lo .iterator() di uno stream non lo supportano. Non puoi nemmeno chiamare remove() due volte di fila senza una next() intermedia — questo lancerebbe IllegalStateException.
Iterator<String> it = names.iterator();
while (it.hasNext()) {
String name = it.next();
if (name.isEmpty()) it.remove(); // legal, fail-safe removal
}it.remove() è l'unico modo sicuro per rimuovere da una collezione durante l'iterazione con un Iterator semplice. Il metodo remove(...) della collezione stessa invaliderebbe l'iteratore e lancerebbe ConcurrentModificationException alla chiamata successiva.
Iterazione fail-fast
La maggior parte degli iteratori delle collezioni JDK sono fail-fast: registrano il conteggio delle modifiche della collezione quando l'iteratore viene creato, lo verificano a ogni chiamata di hasNext/next, e lanciano ConcurrentModificationException se è cambiato da qualcuno diverso dall'iteratore stesso.
List<String> names = new ArrayList<>(List.of("a", "b", "c"));
Iterator<String> it = names.iterator();
names.add("d"); // direct mutation, not via iterator
it.next(); // throws ConcurrentModificationExceptionFail-fast è una diagnostica best effort, non una garanzia di sicurezza nei thread. Intercetta il bug comune ("oh, ho modificato la lista dentro il ciclo e ora il mio iteratore è confuso") in modo chiaro e tempestivo. Non protegge contro la modifica concorrente da un altro thread — per quello è necessaria una collezione concorrente (CopyOnWriteArrayList, ConcurrentHashMap) i cui iteratori sono invece debolmente consistenti: scorrono uno snapshot e non lanciano mai eccezioni.
forEachRemaining e forEach
Due metodi default rendono l'iterazione più concisa quando non hai bisogno del cursore:
list.forEach(System.out::println); // every element
Iterator<String> it = list.iterator();
while (it.hasNext() && !it.next().equals("STOP")) { }
it.forEachRemaining(System.out::println); // everything past STOPforEach è su Iterable; forEachRemaining è su Iterator. Entrambi sono sequenziali. Non usarli quando hai anche bisogno di remove — nascondono il cursore, e remove lo richiede.
Scrivere il tuo Iterator
Ne scriverai uno quando implementi un tipo personalizzato simile a una collezione. Il contratto è piccolo, ma ogni sua parte è importante:
class Countdown implements Iterable<Integer> {
private final int from;
Countdown(int from) { this.from = from; }
@Override public Iterator<Integer> iterator() {
return new Iterator<>() {
int n = from;
@Override public boolean hasNext() { return n > 0; }
@Override public Integer next() {
if (n <= 0) throw new NoSuchElementException();
return n--;
}
};
}
}
for (int x : new Countdown(3)) System.out.println(x); // 3 2 1Tre cose da fare correttamente:
next()deve lanciareNoSuchElementExceptionquando esaurito. Non restituirenullo un valore sentinella.- L'iteratore deve essere una nuova istanza a ogni chiamata di
iterator(). Chiamarefor (... : it)due volte sullo stesso iterable dovrebbe entrambe le volte partire dall'inizio. remove()è opzionale. Non implementarlo a meno che tu non possa farlo davvero — il corpodefaultche lancia è corretto.
Un esempio completo: iterazione, rimozione, fail-fast, iterable personalizzato
Il programma sottostante percorre un ArrayList in tre modi (for-each, iteratore esplicito, forEachRemaining), rimuove elementi in modo sicuro con Iterator.remove, dimostra l'eccezione fail-fast quando si bypassa l'iteratore, e termina con un piccolo Iterable<T> personalizzato.
Cosa ricavare dall'esecuzione:
- Il ciclo for-each ha stampato ogni elemento; dietro le quinte ha chiesto all'
ArrayListun iteratore e lo ha percorso conhasNext/next. Iterator.removeha eliminato le stringhe vuote durante l'iterazione senza unaConcurrentModificationException. Questa è l'unica tecnica corretta di eliminazione nel ciclo con unIteratorsemplice.forEachRemainingè un modo elegante per drenare tutto ciò che l'iteratore non ha ancora restituito — utile subito dopo un percorso parziale.- Mutare direttamente la lista mentre un altro iteratore era attivo ha lanciato
ConcurrentModificationExceptionalla successiva chiamata dinext(). L'eccezione è intenzionale: rende il bug evidente. - Il
Countdownpersonalizzato mostra il contratto minimo necessario per scrivere un iterable funzionante.hasNextsegnala correttamente;nextlancia quando esaurito; nessunremove(eredita quello di default).
Cosa viene dopo
Un Iterator semplice può scorrere in avanti e rimuovere. È sufficiente per set, map e queue — non hanno posizioni in nessun altro senso. Le liste sì, e hanno un cursore più ricco: ListIterator<E> può muoversi in avanti e all'indietro, riportare indici, e add o set elementi durante lo scorrimento. Questo è il prossimo capitolo.