W3docs

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 pattern

remove() 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 ConcurrentModificationException

Fail-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 STOP

forEach è 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 1

Tre cose da fare correttamente:

  1. next() deve lanciare NoSuchElementException quando esaurito. Non restituire null o un valore sentinella.
  2. L'iteratore deve essere una nuova istanza a ogni chiamata di iterator(). Chiamare for (... : it) due volte sullo stesso iterable dovrebbe entrambe le volte partire dall'inizio.
  3. remove() è opzionale. Non implementarlo a meno che tu non possa farlo davvero — il corpo default che 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.

java— editable, runs on the server

Cosa ricavare dall'esecuzione:

  • Il ciclo for-each ha stampato ogni elemento; dietro le quinte ha chiesto all'ArrayList un iteratore e lo ha percorso con hasNext/next.
  • Iterator.remove ha eliminato le stringhe vuote durante l'iterazione senza una ConcurrentModificationException. Questa è l'unica tecnica corretta di eliminazione nel ciclo con un Iterator semplice.
  • 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 ConcurrentModificationException alla successiva chiamata di next(). L'eccezione è intenzionale: rende il bug evidente.
  • Il Countdown personalizzato mostra il contratto minimo necessario per scrivere un iterable funzionante. hasNext segnala correttamente; next lancia quando esaurito; nessun remove (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.

Pratica

Pratica
All'interno di un ciclo `for-each` su `List<String> list`, chiami `list.remove(name)` per eliminare le voci corrispondenti. La prima rimozione funziona; l'iterazione successiva lancia un'eccezione. Qual è la soluzione corretta?
All'interno di un ciclo `for-each` su `List<String> list`, chiami `list.remove(name)` per eliminare le voci corrispondenti. La prima rimozione funziona; l'iterazione successiva lancia un'eccezione. Qual è la soluzione corretta?
Was this page helpful?