Costrutti
Iterativi
Dopo
i costrutti condizionali, l'altro costrutto fondamentale
della programmazione è quello iterativo, il cui
più celebre rappresentate è il ciclo while.
Sebbene il while sia tecnicamente in grado di risolvere
qualunque situazione di iterazione, esistono altri due
costrutti, che in taluni casi risultano più facili
da usare: il ciclo do-while e il ciclo for. Ne paragrafi
successivi li analizzeremo tutti e tre.
Ciclo
while
La
struttura fondamentale del ciclo while è:
while(condizioneBooleana)
istruzione;
come
di consueto, l'istruzione può essere sostituita
da un blocco:
while(condizioneBooleana)
{
istruzione_1;
istruzione_2;
....
istruzione_n;
}
L'effetto
del while è quello di ripetere l'istruzione,
o il blocco di istruzioni, fintanto che il valore della
condizione booleana è vero. Il blocco di istruzioni
all'interno del while deve contenere istruzioni che
modifichino la condizione booleana, in caso contrario
il ciclo durerà all'infinito.
L'uso
del while richiede alcune precauzioni: nel progetto
di un ciclo while è necessario infatti considerare
con attenzione le condizioni di ingresso e quelle di
uscita. Se le condizioni di ingresso sono mal formulate,
il computer in fase di esecuzione non entrerà
mai in ciclo. Se d'altra parte esiste un errore di logica
all'interno del ciclo, si corre il rischio che il computer
entri in loop, ossia resti intrappolato all'infinito
dentro al ciclo. D'altra parte ci sono situazioni in
cui si desidera creare esplicitamente un ciclo infinito:
in questi casi si può ricorrere ad una formulazione
del tipo:
while(true)
istruzione;
Ciclo
do - while
Nel
ciclo while standard, descritto nel paragrafo precedente,
il blocco di istruzioni che costituiscono il corpo del
while può non essere mai eseguito, qualora le
condizioni di ingresso non siano vere al momento di
entrare in ciclo. Esistono casi in cui si vorrebbe che
tali istruzioni vengano eseguite comunque almeno una
volta. In questo caso si può usare il costrutto
do - while, la cui struttura è:
do
istruzione;
while(condizioneBooleana)
o
in alternativa
do
{
istruzione_1;
istruzione_2;
....
istruzione_n;
}
while(condizioneBooleana)
se
si desidera che venga eseguito un blocco di istruzioni.
Il
costrutto do - while è presente in tutti i linguaggi
di programmazione imperativi, a volte con il nome di
repeat-until, come nel Pascal. Esso, in ogni caso, risulta
essere un costrutto poco utilizzato.
Ciclo
for
Se
ci troviamo a dover ordinare l'esecuzione di un blocco
di codice per un numero prefissato di volte possiamo
utilizzare un ciclo while di questo tipo:
int
i = 0;
while(i < 10) {
System.out.println(i);
i++;
}
Il
linguaggio Java, al pari dei più importanti linguaggi
di programmazione, prevede il costrutto for, che permette
di gestire esplicitamente questo tipo di situazione,
peraltro estremamente comune. La sintassi generale del
ciclo for è:
for
( inizializzatore ; condizioneBoleana ; incremento)
istruzione;
Anche
in questo caso è possibile ricorrere al blocco
qualora si desideri far ripetere un maggior numero di
istruzioni:
for
( inizializzatore ; condizioneBoleana ; incremento)
{
istruzione_1;
istruzione_2;
....
istruzione_n;
}
L'inizializzatore
è un'istruzione che viene eseguita una sola volta
prima di entrare nel ciclo. La condizione booleana è
un'espressione booleana che viene testata, come nel
while, prima di ogni ciclo; infine abbiamo un'istruzione
di incremento che viene eseguita automaticamente al
termine di ogni iterazione.
Il
for permette di riscrivere il precedente esempio in
una forma più compatta:
for
( int i = 0 ; i < 10 ; i++ )
System.out.println(i);
Grazie
al for, è possibile gestire in modo molto più
sicuro tutte le circostanze in cui sia necessario far
ripetere un gruppo di istruzioni per un numero prefissato
di volte. Come vedremo nei prossimi paragrafi, tuttavia,
è possibile fare un uso più sofisticato
di questo costrutto.
Uso
del for con parametri multipli
E'
possibile fare in modo che il ciclo for lavori su più
di un contatore, replicando l'inizializzatore e l'incremento
separandoli con la virgola. Ovviamente questo richiede
una attenzione maggiore nel progetto del ciclo, la cui
complessità cresce enormemente con il crescere
del numero dei parametri. Nel seguente esempio abbiamo
una variabile i che assume i valori tra 0 e 59 con incrementi
unitari, mentre la variabile j parte da 1 e viene ad
ogni ciclo moltiplicata per 2:
public
class PotenzeDiDue {
public static void main(String argv[]) {
for ( long i = 0, j = 1; i <
60; i++, j *= 2)
System.out.println("2
elevato alla " + i + " = " + j);
}
}
Si
noti che, nel precedente esempio, la variabile j non
viene valutata all'interno della condizione booleana:
in altre parole, la durata del ciclo viene stabilita
unicamente attraverso la variabile i. Pertanto è'
possibile riscrivere l'esempio precedente ricorrendo
ad un unico contatore tra i parametri del ciclo for,
trasferendo l'istruzione di incremento della variable
j all'interno del corpo del ciclo:
public
class PotenzeDiDue {
public static void main(String argv[]) {
long j = 1;
for ( long i = 0 ; i < 60;
i++) {
System.out.println("2
elevato alla " + i + "=" + j);
j *= 2;
}
}
}
Quale
delle due forme scegliere è qualcosa che il programmatore
è chiamato a decidere caso per caso, tenendo
presente che spesso è preferibile privilegiare
la chiarezza alla concisione (in altre parole: meglio
scrivere più righe che correre il rischio di
introdurre errori).
Omissione
di parametri
E'
possibile lasciare in bianco uno o più parametri
del for. Se la variabile che si usa come indice è
già stata dichiarata, è possibile omettere
l'inizializzazione:
int
i = 0;
for ( ; i < 10 ; i++)
System.out.println(i);
Se
l'istruzione di incremento è presente all'interno
del ciclo, si può lasciare in bianco il terzo
parametro:
for
( int i = 0 ; i < 10 ; ) {
System.out.println(i);
i++;
}
Infine,
se si lascia in bianco la condizione booleana, il ciclo
verrà eseguito all'infinito:
for
( int i = 0 ; ; i++ )
System.out.println(i);
Ciascuno
di questi casi può essere gestito con un while
equivalente, cosa che in genere risulta preferibile
perché più facile da capire.
Cicli
nidificati
All'interno
di un ciclo possiamo inserire un ulteriore ciclo, ottenendo
in tal modo una struttura di controllo nidificata. L'uso
di cicli nidificati è indispensabile a molte
importanti tecniche di calcolo: si pensi al calcolo
vettoriale, che stà alla base dei giochi 3D e
degli effetti speciali cinematografici. D'altra parte
l'uso di cicli nidificati richiede una notevole attenzione,
dato che proprio in simili situazioni è facile
introdurre errori di logica che portano al blocco del
programma. Vediamo un algoritmo che riempie una matrice
11 x 11 con i valori della tabellina del 10. Il ciclo
più esterno scandisce le righe della matrice;
ad ogni iterazione viene avviato un ciclo interno che
riempie tutti gli elementi della riga:
int[][]
tabellina = new int[11][11];
for ( int i = 0; i < 11; i++) // ciclo esterno
for ( int j = 0; j < 11; j++) // ciclo
interno
tabellina[i][j] = i * j;
Si
noti che il ciclo più esterno conta 10 iterazioni;
ad ognuna di esse viene eseguito per intero il ciclo
più interno, che conta altre 10 iterazioni. In
totale l'istruzione
tabellina[i][j]
= i * j;
viene
eseguita 100 volte.
Uso
di break
L'istruzione
break, quando sia presente all'interno di un ciclo while,
do - while o for, ha l'effetto di interrompere l'iterazione,
e di ricominciare dall'istruzione immediatamente sucessiva.
L'istruzione break consente di inserire condizioni di
uscita supplementari, localizzate in punti diversi dall'inizio
o dalla fine del ciclo.
Un
uso tipico dell'istruzione break è all'interno
di algoritmi di ricerca lineare, algoritmi che scandiscono
uno ad uno gli elementi di un vettore fino a quando
non viene trovato un particolare valore. In prima istanza,
una valida soluzione al problema è un algoritmo
del tipo:
boolean
found = false;
for ( int i = 0; i < array.length ; i++ )
if(array[i] == 101)
found = true;
if(found)
System.out.println("Il
vettore contiene il valore 101");
Dato
un array di interi, lo si scandisce dal primo all'ultimo
elemento, controllando se uno di essi ha valore 101.
Se lo si trova, la variabile booleana found assume il
valore true; in caso contrario al termine del ciclo
esso conterrà il valore false. L'ultima istruzione
stamperà una scritta se la variabile found è
true. Con un algoritmo di questo tipo, se il vettore
è molto grande (immaginiamo un vettore da 10
o 20 milioni di elementi) e il valore viene trovato
dopo appena un centinaio di iterazioni, tutte le successive
iterazioni risulteranno inutili. Inserendo un'istruzione
break, facciamo in modo che il ciclo duri il minimo
necessario per trovare l'elemento, cosa che spesso porta
ad un netto incremento delle prestazioni:
boolean
found = false;
for ( int i = 0; i < array.length ; i++ )
if(array[i] == 101) {
found = true;
break;
// il ciclo finisce qui
}
if(found)
System.out.println("Il
vettore contiene il valore 101");
L'istruzione
continue
L'istruzione
continue ha un effetto simile a quello di break, ma
invece di dirottare l'esecuzione al termine del ciclo,
la riporta all'inizio e passa alla successiva iterazione.
Così, se dobbiamo realizzare un ciclo che effettui
il prodotto di tutti i valori di un vettore, trascurando
gli elementi con valore zero, possiamo scrivere:
long
prodotto = 0;
for ( int i = 0; i < array.length ; i++ ) {
if(array[i]
== 0)
//
salta direttamente alla successiva iterazione
continue;
prodotto
= prodotto * array[i];
}
Uso
di break e continue all'interno di cicli nidificati
Quale
effetto hanno le istruzioni break e continue se presenti
in un sistema di cicli nidificati? Tali istruzioni hanno
effetto solo sul ciclo corrente. Se collochiamo un'istruzione
break in un ciclo esterno, essa provocherà la
fine di entrambi i cicli e la prosecuzione del programma,
mentre se poniamo il break in un ciclo interno, esso
provocherà l'interruzione del solo ciclo interno,
e la prosecuzione di quello esterno:
for
( int i = 0; i < 11; i++) {
//
ciclo esterno
....
for
( int j = 0; j < 11; j++) {
//
ciclo interno
...
break;
// interrompe solo il ciclo interno
}
// dopo il break si riparte da qui
....
}
Un
discorso del tutto simile vale per il continue:
for
( int i = 0; i < 11; i++) {
// ciclo esterno
....
for ( int j = 0; j < 11; j++) { // ciclo
interno
...
//
va alla successiva iterazione del ciclo interno
continue;
...
}
....
}
Uso
di break e continue con label
Le
istruzioni break e continue possono essere utilizzate
in modo più avanzato grazie alle label. Una label
è un identificatore seguito da un carattere ':'
e da un'istruzione o da un blocco di istruzioni tra
parentesi graffe:
nome
: istruzione;
L'istruzione,
che in questa circostanza prende il nome di istruzione
etichettata (labeled statement), può essere una
qualunque istruzione Java, compreso un ciclo.
L'istruzione
break può specificare una label, in modo da precisare
quale istruzione si intenda interrompere. Come già
visto, l'effetto standard del break è quello
di interrompere il ciclo corrente; grazie al break con
etichetta, possiamo riproporre una variazione dell'esempio
precedente in cui il break, sebbene posto nel ciclo
più interno, provoca l'interruzione di quello
più esterno. Per ottenere questo effetto, dobbiamo
etichettare il ciclo più esterno con un nome
(cosa che viene fatta nella prima riga), e specificare,
dopo il break, l'etichetta assegnata al ciclo più
esterno:
cicloEsterno :
for ( int i = 0; i < 11; i++) { // ciclo esterno
....
for ( int j = 0; j < 11; j++) { // ciclo interno
...
break cicloEsterno; // interrompe il ciclo esterno
}
....
}
// dopo il break si riparte da qui
Anche
l'istruzione continue prevede la variante con label:
in questo caso l'etichetta permette di indicare quale
ciclo si intende proseguire:
cicloEsterno :
for ( int i = 0; i < 11; i++) { // ciclo esterno
// dopo il continue si continua da qui
....
for ( int j = 0; j < 11; j++) { // ciclo interno
...
continue cicloEsterno; // riprende il ciclo esterno
}
....
}
Inutile
sottolineare come l'uso di queste istruzioni tenda a
complicare enormemente i programmi, laddove, nella maggior
parte dei casi, è possibile formulare un costrutto
equivalente. Per questa ragione non capita spesso di
incontrare labeled statement nei programmi Java.
Conclusioni
Questo
mese abbiamo appreso il funzionamento dettagliato dei
costrutti iterativi in Java. Il mese prossimo vedremo
come sia possibile, attraverso i cosiddetti metodi,
organizzare il codice in moduli, per aumentarne l'efficinza
e la chiarezza: sarà questo l'ultimo passo da
compiere prima di poter cominciare a parlare seriamente
di Programmazione Orientata agli Oggetti.
Bibliografia
Risorse
Scarica
qui l'archivio con i sorgenti presentati nell'articolo
|