Accedere ad una Collezione usando gli Iteratori in Java

Concetti Chiave
  • Gli iteratori forniscono un modo per accedere agli elementi di una collezione in modo sequenziale senza esporre la sua rappresentazione interna.
  • Le interfacce Iterator e ListIterator consentono di scorrere gli elementi di una collezione, con ListIterator che offre funzionalità aggiuntive per l'attraversamento bidirezionale e la modifica degli elementi.
  • Gli iteratori possono essere utilizzati in modo più flessibile rispetto ai cicli tradizionali, consentendo di rimuovere elementi durante l'iterazione.
  • Esiste un'alternativa più semplice agli iteratori, il ciclo for-each, che può essere utilizzato per scorrere le collezioni senza modificarle.

Accesso ad una Collezione con un Iterator

Spesso, sorge la necessità di scorrere gli elementi in una collezione.

Ad esempio, si potrebbe voler visualizzare ogni elemento. Un modo per farlo è utilizzare un iterator, che è un oggetto che implementa l'interfaccia Iterator o ListIterator.

Iterator vi consente di scorrere una collezione, ottenendo o rimuovendo elementi. ListIterator estende Iterator per consentire l'attraversamento bidirezionale di una lista e la modifica degli elementi. Iterator e ListIterator sono interfacce generiche che sono dichiarate come mostrato qui:

interface Iterator<E>
interface ListIterator<E>

Qui, E specifica il tipo di oggetti su cui si vuole iterare.

L'interfaccia Iterator dichiara i metodi mostrati nella tabella che segue:

Metodo Descrizione
default void forEachRemaining(Consumer<? super E> action) L'azione specificata da action viene eseguita su ogni elemento non elaborato nella collezione.
boolean hasNext() Restituisce true se ci sono più elementi. Altrimenti, restituisce false.
E next() Restituisce l'elemento successivo. Lancia NoSuchElementException se non c'è un elemento successivo.
default void remove() Rimuove l'elemento corrente. Lancia IllegalStateException se viene fatto un tentativo di chiamare remove() che non è preceduto da una chiamata a next(). La versione predefinita lancia una UnsupportedOperationException.
Tabella 1: Metodi dichiarati dall'interfaccia Iterator.

I metodi dichiarati da ListIterator sono mostrati nella tabella seguente:

Metodo Descrizione
void add(E obj) Inserisce obj nella lista davanti all'elemento che sarà
restituito dalla prossima chiamata a next().
default void forEachRemaining(Consumer<? super E> action) L'azione specificata da action viene eseguita su ogni elemento non elaborato nella collezione.
boolean hasNext() Restituisce true se c'è un elemento successivo. Altrimenti, restituisce false.
boolean hasPrevious() Restituisce true se c'è un elemento precedente. Altrimenti, restituisce false.
E next() Restituisce l'elemento successivo. Viene lanciata una NoSuchElementException se non c'è un elemento successivo.
int nextIndex() Restituisce l'indice dell'elemento successivo. Se non c'è un elemento successivo, restituisce la dimensione della lista.
E previous() Restituisce l'elemento precedente. Viene lanciata una NoSuchElementException se non c'è un elemento precedente.
int previousIndex() Restituisce l'indice dell'elemento precedente. Se non c'è un elemento precedente, restituisce –1.
void remove() Rimuove l'elemento corrente dalla lista. Viene lanciata una IllegalStateException se remove() viene chiamato prima che next() o previous() vengano invocati.
void set(E obj) Assegna obj all'elemento corrente. Questo è l'elemento restituito per ultimo da una chiamata a next() o previous().
Tabella 2: Metodi dichiarati dall'interfaccia ListIterator.

In entrambi i casi, le operazioni che modificano la collezione sottostante sono opzionali. Ad esempio, remove() lancerà UnsupportedOperationException quando utilizzato con una collezione di sola lettura. Sono possibili varie altre eccezioni.

Utilizzare un Iterator

Prima di poter accedere ad una collezione tramite un iterator, occorre ottenerne uno.

Ciascuna delle classi collection fornisce un metodo iterator() che restituisce un iterator all'inizio della collezione. Utilizzando questo oggetto iterator, è possibile accedere a ciascun elemento nella collezione, un elemento alla volta.

In generale, per utilizzare un iterator per scorrere il contenuto di una collezione, seguiamo questi passaggi:

    1. Otteniamo un iterator all'inizio della collezione chiamando il metodo iterator() della collezione.
    1. Impostiamo un loop che effettua una chiamata a hasNext(). Facciamo iterare il loop finché hasNext() restituisce true.
    1. All'interno del loop, otteniamo ciascun elemento chiamando next().

Per le collezioni che implementano List, possiamo anche ottenere un iterator chiamando listIterator(). Come spiegato, un list iterator offre la capacità di accedere alla collezione in direzione sia avanti che indietro e permette di modificare un elemento. Per il resto, ListIterator viene utilizzato proprio come Iterator.

L'esempio seguente implementa questi passaggi, dimostrando sia le interfacce Iterator che ListIterator. Utilizza un oggetto ArrayList, ma i principi generali si applicano a qualsiasi tipo di collezione. Naturalmente, ListIterator è disponibile solo per quelle collezioni che implementano l'interfaccia List.

// Dimostrare gli iterator.
import java.util.*;

class DemoIterator {

    public static void main(String[] args) {
        // Crea un array list.
        ArrayList<String> al = new ArrayList<String>();

        // Aggiunge elementi all'array list.
        al.add("C");
        al.add("A");
        al.add("E");
        al.add("B");
        al.add("D");
        al.add("F");

        // Utilizza iterator per visualizzare il contenuto di al.
        System.out.print("Contenuto originale di al: ");
        Iterator<String> itr = al.iterator();
        while (itr.hasNext()) {
            String elemento = itr.next();
            System.out.print(elemento + " ");
        }
        System.out.println();

        // Modifica oggetti durante l'iterazione.
        ListIterator<String> litr = al.listIterator();
        while (litr.hasNext()) {
            String elemento = litr.next();
            litr.set(elemento + "+");
        }
        System.out.print("Contenuto modificato di al: ");
        itr = al.iterator();
        while (itr.hasNext()) {
            String elemento = itr.next();
            System.out.print(elemento + " ");
        }
        System.out.println();

        // Ora, visualizza la lista all'indietro.
        System.out.print("Lista modificata all'indietro: ");
        while (litr.hasPrevious()) {
            String elemento = litr.previous();
            System.out.print(elemento + " ");
        }
        System.out.println();
    }

}

L'output è mostrato qui:

Contenuto originale di al: C A E B D F 
Contenuto modificato di al: C+ A+ E+ B+ D+ F+ 
Lista modificata all'indietro: F+ D+ B+ E+ A+ C+

Prestiamo particolare attenzione a come la lista viene visualizzata al contrario. Dopo che la lista è stata modificata, litr punta alla fine della lista. Ricordiamo, litr.hasNext() restituisce false quando è stata raggiunta la fine della lista. Per attraversare la lista in reverse, il programma continua ad utilizzare litr, ma questa volta controlla se ha un elemento precedente. Finché ne ha uno, quell'elemento viene ottenuto e visualizzato.

L'Alternativa For-Each agli Iteratori

Se non si modificano i contenuti di una collection o non si vuole ottenere gli elementi in ordine inverso, allora la versione for-each del ciclo for è spesso un'alternativa più conveniente per scorrere una collection rispetto all'uso di un iteratore.

Si ricordi che il for può scorrere qualsiasi collection di oggetti che implementa l'interfaccia Iterable. Poiché tutte le classi collection implementano questa interfaccia, possono tutte essere elaborate dal for.

L'esempio seguente usa un ciclo for per sommare i contenuti di una collection:

// Usa il ciclo for-each per scorrere una collection.
import java.util.*;

class DemoForEach {

    public static void main(String[] args) {
        // Crea un array list per gli interi.
        ArrayList<Integer> valori = new ArrayList<Integer>();

        // Aggiungi valori all'array list.
        valori.add(1);
        valori.add(2);
        valori.add(3);
        valori.add(4);
        valori.add(5);

        // Usa il ciclo for per visualizzare i valori.
        System.out.print("Contenuti di valori: ");
        for (int v : valori)
            System.out.print(v + " ");
        System.out.println();

        // Ora, somma i valori usando un ciclo for.
        int somma = 0;
        for (int v : valori)
            somma += v;
        System.out.println("Somma dei valori: " + somma);
    }

}

L'output del programma è mostrato qui:

Contenuti di valori: 1 2 3 4 5 
Somma dei valori: 15

Come potete vedere, il ciclo for è sostanzialmente più breve e semplice da usare rispetto all'approccio basato su iteratore. Tuttavia, può essere usato solo per scorrere una collection nella direzione in avanti, e non potete modificare i contenuti della collection.