Java – Guida al costrutto “for-each”: gli array

In questa seconda parte della mia Guida al costrutto “for-each” descriverò l’utilizzo del ciclo for-each con gli array. Come ho già anticipato nella precedente introduzione, il for-each può operare su array di qualunque tipo, sia di tipo primitivo che di tipo reference.

Introduzione

Prendiamo come esempio i due seguenti array, uno di valori primitivi (int) e l’altro di valori reference (oggetti String):

int[] numeri = { 10, 20, 30 };
String[] nomi = { "Anna", "Luigi", "Paolo" };

Questo è ciò che si poteva fare prima di Java 5 per stampare i valori:

for (int i = 0; i < numeri.length; i++) {
    System.out.println(numeri[i]);
}

for (int i = 0; i < nomi.length; i++) {
    System.out.println(nomi[i]);
}

Questo invece è l’equivalente che si può fare con il nuovo for-each disponibile da Java 5:

for (int numero : numeri) {
    System.out.println(numero);
}

for (String nome : nomi) {
    System.out.println(nome);
}

Come si può osservare, la forma è decisamente più chiara, snella e concisa. Il fatto di non dover più fare la iterazione con l’indice permette anche di evitare i “classici” errori come ad esempio il seguente:

for (int i = 0; i <= numeri.length; i++)

Notate l’errore? Questo è uno dei tipici errori che si possono commettere in questi casi (e specialmente se si è agli inizi con Java). Il <= fa sì che la variabile i possa arrivare anche al valore di numeri.length, che però è un indice “fuori” dall’array e causa quindi una eccezione ArrayIndexOutOfBoundsException.

Un’altra cosa da notare, come già detto nella introduzione, è che i due identificatori numero e nome sono le variabili che è obbligatorio dichiarare nel for-each. È infatti illegale fare ad esempio:

int numero;
for (numero : numeri) {     // NO, errore!
    System.out.println(numero);
}

La variabile deve essere obbligatoriamente dichiarata nel costrutto for-each, non si può riutilizzare una variabile definita in precedenza (indipendentemente dal fatto che sia già stata inizializzata oppure no e che sia già stata usata per altro oppure no).

Forma generalizzata

Per comprendere meglio il for-each sugli array è bene vedere che cosa viene generato. Quando c’è un for-each che ha come “target” un array, il compilatore genera un codice che ha la seguente forma generalizzata:

tipo[] #a = espressione;
label1: label2: ... labelN:
for (int #i = 0; #i < #a.length; #i++) {
    [modificatori] tipo identificatore = #a[#i];
    istruzione
}

Se questa forma generalizzata non fosse completamente chiara, non è un grosso problema. Ci sono principalmente solo 3 cose importanti da notare:

  1. Il compilatore genera un codice che sfrutta proprio il ciclo for “classico” con l’indice. In pratica è sostanzialmente lo stesso codice che si potrebbe scrivere “a mano”.
  2. Ci sono due variabili particolari #a e #i che sono denominate così solo per indicare che in realtà sono generate automaticamente dal compilatore in modo che siano distinte da qualunque altra variabile presente nel codice.
  3. La variabile dichiarata nel for-each riceve di volta in volta una copia del valore di un elemento dell’array.

Limiti e restrizioni

I punti 2 e 3 del precedente elenco in particolare hanno delle implicazioni molto importanti che causano alcuni limiti e restrizioni nell’uso del for-each che è bene conoscere.

La variabile di indice non è accessibile

La variabile di indice, essendo generata arbitrariamente dal compilatore, non è nota a priori e quindi non è usabile nel sorgente. Questo impedisce di conoscere l’indice e di usarlo per fare delle logiche di test o controllo particolari. E inoltre non permette di aggiornare l’elemento i-esimo dell’array.

Se proprio servisse davvero avere un indice, si potrebbe definire una variabile “extra” di indice da gestire esplicitamente, come ad esempio:

int indice = 0;
for (int numero : numeri) {
    // ..... usa anche indice
    indice++:
}

Se però si deve ricorrere a tale soluzione, specialmente se il corpo del ciclo fosse un po’ lungo o se ci fossero logiche particolari sugli indici da fare, probabilmente sarebbe meglio tornare al caro vecchio ciclo for “classico” con l’indice. Questa comunque è una questione da valutare caso per caso.

Gli elementi non sono aggiornabili tramite la variabile dichiarata

Poiché la variabile dichiarata nel for-each è ben distinta dall’array e riceve una copia del valore di ciascun elemento, assegnare qualcosa di diverso a questa variabile non ha alcun effetto sull’array e sui suoi elementi.

I due seguenti sono esempi di un uso errato e improprio del for-each:

for (int numero : numeri) {
    numero = numero * 2;   // NO, non cambia i valori nell’array!!
}
for (String nome : nomi) {
    nome = nome.toUpperCase();   // NO, non cambia i nomi nell’array!!
}

In particolare la invocazione di toUpperCase() non ha effetto principalmente perché gli oggetti String sono “immutabili”. Il metodo toUpperCase() infatti restituisce un nuovo oggetto String e, come appena detto, assegnare qualcos’altro alla variabile del for-each non ha effetto sull’elemento dell’array.

La questione invece sarebbe ben diversa se si trattasse di un array di oggetti “mutabili”. In tal caso sarebbe assolutamente possibile invocare uno o più metodi su ciascun oggetto per modificare il suo “stato”. Un esempio abbastanza eloquente si può fare con la classe java.awt.Point che rappresenta un punto x,y (su un piano 2D) “mutabile”.

Point[] punti = { new Point(3, 4), new Point(7, 2) };

for (Point punto : punti) {
    punto.translate(2, -3);     // Ok, sposta ciascun punto di +2 su x e -3 su y
}

In quest’ultimo codice gli elementi dell’array non vengono modificati (gli oggetti restano esattamente quelli creati all’inizio). Quello che invece viene modificato è lo “stato” (l’insieme delle variabili di “istanza”) di ciascun oggetto Point.

Esempio completo

Il seguente è un piccolo esempio completo che sfrutta oltretutto un doppio ciclo for-each “annidato”:

public class CombinazioniNomeCognome {
    public static void main(String[] args) {
        String[] nomi = { "Anna", "Luigi", "Paolo" };
        String[] cognomi = { "Bianchi", "Rossi", "Verdi" };

        for (String nome : nomi) {
            for (String cognome : cognomi) {
                System.out.println(nome + " " + cognome);
            }
        }
    }
}

Conclusioni

Il ciclo for-each introdotto in Java 5 è un costrutto interessante, valido e utile in generale quando si deve semplicemente “scorrere” un array dal primo all’ultimo elemento senza altre particolari questioni.

Invece non va bene quando si ha una (o più) delle seguenti necessità:

  • si vuole scorrere l’array al contrario (dall’ultimo al primo elemento) e/o a passi di n indici.
  • si vuole usare il valore dell’indice
  • si vuole cambiare i valori degli elementi nell’array

In tutti questi casi è certamente più utile scrivere direttamente il ciclo for “classico”.