Cicli Innestati in Java

Concetti Chiave
  • I cicli innestati in Java permettono di eseguire operazioni complesse iterando su più dimensioni, come ad esempio la stampa di tabelle o l'elaborazione di stringhe.
  • La sintassi di Java consente di annidare strutture di controllo, come cicli for o istruzioni if, per gestire situazioni più complesse.

Cicli innestati

Le strutture di controllo in Java sono istruzioni che contengono altre istruzioni più semplici.

In particolare, una struttura di controllo può contenerne un'altra. Sono già stati mostrati diversi esempi di istruzioni if all'interno di cicli e un esempio di ciclo while all'interno di un altro ciclo while, ma è possibile qualsiasi combinazione di una struttura di controllo annidata in un'altra.

Si dice che una struttura è annidata o innestata in un'altra. È anche possibile avere più livelli di annidamento, ad esempio un ciclo while all'interno di un'istruzione if che a sua volta si trova all'interno di un altro ciclo while. La sintassi di Java non impone un limite al numero di livelli di annidamento. Dal punto di vista pratico, tuttavia, risulta difficile comprendere un programma con più di pochi livelli di annidamento ed è una regola di stile evitare un annidamento eccessivo.

I cicli for annidati sorgono naturalmente in molti algoritmi ed è importante comprenderne il funzionamento. Si considerino un paio di esempi. Per prima cosa, si ponga il problema di stampare una tabella di moltiplicazione come la seguente:

 1   2   3   4   5   6   7   8   9  10  11  12
 2   4   6   8  10  12  14  16  18  20  22  24
 3   6   9  12  15  18  21  24  27  30  33  36
 4   8  12  16  20  24  28  32  36  40  44  48
 5  10  15  20  25  30  35  40  45  50  55  60
 6  12  18  24  30  36  42  48  54  60  66  72
 7  14  21  28  35  42  49  56  63  70  77  84
 8  16  24  32  40  48  56  64  72  80  88  96
 9  18  27  36  45  54  63  72  81  90  99 108
10  20  30  40  50  60  70  80  90 100 110 120
11  22  33  44  55  66  77  88  99 110 121 132
12  24  36  48  60  72  84  96 108 120 132 144

I dati della tabella sono disposti in 12 righe e 12 colonne. Il processo di stampa può essere espresso in pseudocodice nel modo seguente:

per ognuno numeroRiga = 1, 2, 3, …, 12:
    stampare i primi dodici multipli di numeroRiga su una riga
    emettere un ritorno a capo

Il primo passo del ciclo for può essere a sua volta espresso come un ciclo for. Si può espandere «stampare i primi dodici multipli di numeroRiga su una riga» come:

per N = 1, 2, 3, …, 12:
    stampare N * numeroRiga

Un algoritmo più preciso per stampare la tabella prevede quindi un ciclo for annidato in un altro:

per ognuno numeroRiga = 1, 2, 3, …, 12:
    per N = 1, 2, 3, …, 12:
        stampare N * numeroRiga
    emettere un ritorno a capo

L'output deve essere stampato in colonne ordinate, facendo occupare a ogni numero esattamente quattro spazi. Ciò si ottiene con l'output formattato e lo specificatore %4d. Supponendo che numeroRiga e N siano variabili di tipo int, l'algoritmo può essere espresso in Java come:

for (int numeroRiga = 1; numeroRiga <= 12; numeroRiga++) {
    for (int N = 1; N <= 12; N++) {
        // stampare in colonne di 4 caratteri
        System.out.printf("%4d", N * numeroRiga);  // Nessun ritorno a capo!
    }
    System.out.println();  // Aggiunge un ritorno a capo alla fine della riga.
}

Come si può osservare, abbiamo due cicli for annidati. Il ciclo esterno scorre i numeri da 1 a 12, uno per volta, e il ciclo interno scorre anch'esso da 1 a 12, ma per ogni valore di numeroRiga. Il ciclo interno stampa i multipli di numeroRiga su una riga, mentre il ciclo esterno passa alla riga successiva.

Nel prossimo esempio si passa all'elaborazione di testo. Si consideri il problema di determinare quali delle 26 lettere dell'alfabeto compaiono in una stringa. Ad esempio, le lettere contenute in «Hello World» sono D, E, H, L, O, R e W. Più precisamente, si scriverà un programma che elenca tutte le lettere contenute in una stringa e conta anche il numero di lettere diverse. La stringa verrà inserita dall'utente. Si inizi da un algoritmo in pseudocodice per il programma.

chiedere all'utente di inserire una stringa
leggere la risposta in una variabile, testo
porre contatore = 0   (per contare il numero di lettere diverse)
per ogni lettera dell'alfabeto:
    se la lettera compare in testo:
        stampare la lettera
        aggiungere 1 a contatore
visualizzare contatore

Poiché si deve elaborare l'intera riga di testo inserita dall'utente, si userà TextIO.getln() per leggerla.

La riga dell'algoritmo che recita «per ogni lettera dell'alfabeto» può essere espressa come for (lettera = 'A'; lettera <= 'Z'; lettera++). Occorre però pensare meglio all'istruzione if dentro il ciclo for. Come verificare se la lettera data, lettera, compare in testo? Un'idea consiste nell'esaminare a turno ogni carattere della stringa e verificare se quel carattere è uguale a lettera. Il carattere i-esimo di testo si ottiene con la chiamata testo.charAt(i), dove i varia da 0 a testo.length() - 1.

Vi è un'ulteriore difficoltà: una lettera come A può comparire in testo sia in maiuscolo che in minuscolo, A oppure a. È necessario controllare entrambe le occorrenze, ma si può evitare il problema convertendo testo in maiuscolo prima dell'elaborazione. In tal modo basta verificare la lettera in maiuscolo. L'algoritmo completo diventa quindi:

chiedere all'utente di inserire una stringa
leggere la risposta in una variabile, testo
convertire testo in maiuscolo
porre contatore = 0
per lettera = 'A', 'B', …, 'Z':
    per i = 0, 1, …, testo.length()-1:
        se lettera == testo.charAt(i):
            stampare lettera
            aggiungere 1 a contatore
            // uscire dal ciclo per evitare di contare due volte la lettera
            break
visualizzare contatore
Consiglio

Uso di break nei cicli annidati

Osservare l'uso di break nel ciclo for annidato. È necessario per evitare di stampare o contare una stessa lettera più di una volta (quando compare più volte nella stringa). L'istruzione break esce dal ciclo for interno, ma non da quello esterno. Dopo l'esecuzione di break, l'elaborazione prosegue nel ciclo esterno con il valore successivo di lettera. Occorre stabilire quale sarebbe il valore finale di contatore se l'istruzione break fosse omessa.

Nella lezione successiva studieremo l'istruzione break nel dettaglio.

Di seguito è riportato il programma completo:

import textio.TextIO;

/**
 * Questo programma legge una riga di testo inserita dall'utente.
 * Elenca le lettere che compaiono nel testo
 * e segnala quante lettere diverse sono state trovate.
 */
public class ElencaLettere {

    public static void main(String[] args) {

        String testo;        // Riga di testo inserita dall'utente.
        int   contatore;     // Numero di lettere diverse trovate in testo.
        char  lettera;       // Una lettera dell'alfabeto.

        System.out.println("Digitare una riga di testo.");
        testo = TextIO.getln();

        testo = testo.toUpperCase();

        contatore = 0;
        System.out.println("Il testo inserito contiene le seguenti lettere:");
        System.out.println();
        System.out.print("  ");
        for (lettera = 'A'; lettera <= 'Z'; lettera++) {
            int pos;  // Posizione di un carattere in testo.
            for (pos = 0; pos < testo.length(); pos++) {
                if (lettera == testo.charAt(pos)) {
                    System.out.print(lettera);
                    System.out.print(' ');
                    contatore++;
                    break;
                }
            }
        }

        System.out.println();
        System.out.println();
        System.out.println("Sono state trovate " + contatore + " lettere diverse.");
    }  // fine main()
}      // fine classe ElencaLettere

Esiste in realtà un modo più semplice per stabilire se una determinata lettera compare in una stringa, testo. Il metodo incorporato testo.indexOf(lettera) restituisce -1 se lettera non è presente nella stringa; restituisce invece un numero maggiore o uguale a 0 se la lettera compare. Di conseguenza, si potrebbe verificare la presenza di lettera in testo semplicemente controllando

if (testo.indexOf(lettera) >= 0)

Utilizzando questa tecnica nel programma precedente non sarebbe necessario un ciclo for annidato, mostrando come le sotto-routine aiutino a gestire la complessità.