MokaByte 66 - 7mbre 2002 
Corso introduttivo su Java
VI parte: i costrutti iterativi
di
Andrea Gini
Dopo aver analizzato in profondità l'uso dei costrutti condizionali, è arrivato il momento di approfondire l'uso dei costrutti iterativi. I costrutti iterativi, tra i quali rientra il while, servono ad istruire il calcolatore a ripetere un insieme di istruzioni per un determinato numero di volte. Come vedremo in questo articolo, esistono divese sottigliezze nell'uso dei costrutti iterativi.

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

MokaByte® è un marchio registrato da MokaByte s.r.l. 
Java®, Jini® e tutti i nomi derivati sono marchi registrati da Sun Microsystems.
Tutti i diritti riservati. E' vietata la riproduzione anche parziale.
Per comunicazioni inviare una mail a info@mokabyte.it