Cicli Innestati in Java
- 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 istruzioniif
, 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
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à.