Corso Java - Terza lezione: le strutture principali

Nelle precedenti lezioni, abbiamo sinora fornito fondamentalmente una panoramica della programmazione ad oggetti ed una breve descrizione di come questa si affronti in Java; ci siamo cioè occupati di come deve essere "visto" un problema prima ...
Corso Java - Terza lezione: le strutture principali

Nelle precedenti lezioni, abbiamo sinora fornito fondamentalmente una panoramica della programmazione ad oggetti ed una breve descrizione di come questa si affronti in Java; ci siamo cioè occupati di come deve essere “visto” un problema prima di essere risolto in Java. La scomposizione del problema in oggetti e, quindi, in classi, metodi e attributi è un procedimento che certamente richiede del tempo per essere correttamente assimilato ma è assolutamente fondamentale. Altrettanto basilare è, ovviamente, la conoscenza delle strutture e delle regole principali del linguaggio: esse costituiranno l’oggetto dei prossimi due articoli e non solo.
In questa terza lezione del nostro corso, ci occuperemo di come gestire il flusso di controllo del programma, ovvero delle strutture decisionali e dei cicli.

Strutture decisionali: if e if-else (se…altrimenti…)
Una istruzione decisionale è a tutti gli effetti il modo più semplice che abbiamo di effettuare delle scelte durante l’esecuzione del programma, ad esempio in base a dati immessi dall’utente o da essi derivati o ancora a dati casuali. La valutazione della scelta avviene solo ed esclusivamente in base a espressioni booleane, ovvero espressioni che possono essere vere o false. In particolare, l’istruzione if (o if-else) è l’istruzione più diffusa in tutti i linguaggi di programmazione. Essa è composta dalla parola riservata if seguita da una espressione booleana racchiusa tra parentesi tonde: se questa istruzione assume il valore true (Vero) viene eseguito il codice che segue la if (che può essere, eventualmente, un blocco) altrimenti quel codice non viene eseguito e si passa oltre. Per esempio:

if (a < b)
    System.out.println("Il minore dei due numeri e' a");

supposti a e b numeri, valuta se a è minore di b: se e solo se questo è vero viene stampata a video la stringa desiderata altrimenti il programma prosegue oltre. In questo esempio, poiché la riga da eseguire in base alla if è una sola, non è necessario l’uso delle parentesi graffe che comunque possono essere inserite se migliorano per voi la comprensibilità del codice senza ricevere errori in fase di compilazione o esecuzione del programma.

In alcuni casi abbiamo la necessità di eseguire una certa operazione se e solo se si verifica una condizione ed un’altra operazione se e solo se la condizione non si verifica. Questo può essere fatto in Java (come in molti linguaggi di programmazione) tramite l’istruzione if-else nella quale ciò che segue l’else viene eseguito se e solo se la condizione della if non si verifica. Ad esempio:

if (a < b)
    System.out.println("Il minore dei due numeri e' A");
else
    System.out.println("B e' minore o uguale ad A");

determina il minore tra due numeri e stampa a video un risultato appropriato in base ai dati ingresso. Le più comuni espressioni booleane utilizzate in Java comprendono gli operatori di minore e maggiore () di minore-uguale e maggiore-uguale (=) e gli operatori di uguaglianza, per i quali vale la pena spendere qualche parola. L’operatore di uguaglianza in Java è == (due simboli “uguale”) da non confondersi con l’operatore di assegnamento = (uguale): confondere questi due operatori, cosa che purtroppo avviene spesso anche a programmatori non proprio novizi, è uno degli errori che chi si avvicina al linguaggio e alla programmazione in generale commette più spesso ed è uno degli errori di più difficile soluzione poiché, se i dati che usate nella condizione sono booleani non riceverete nessun errore da parte del compilatore ma molto probabilmente il vostro programma farà una cosa molto diversa da quella per cui era stato progettato.
Poiché abbiamo detto che sia la if che l’else possono essere seguiti da istruzioni o da blocchi di programma, dovrebbe essere chiaro che non vi è alcuna limitazione nello porre altre istruzioni if o if-else all’interno di questi. Questo modo di costruire il programma prende il nome di nidificazione e, in particolare, in questo caso parliamo di if annidati. Ad esempio:

if (a < b)
    System.out.println("Il minore dei due numeri e' A");
else
    if (b < a)
        System.out.println("Il minore dei due numeri e' B");
    else
        System.out.println("I due numeri sono uguali");

valuta esattamente qual è il minore tra due numeri e se essi sono uguali. Bisogna notare come non sia necessario inserire la condizione di uguaglianza ( if(a==b) ) nel nostro codice poiché la struttura nidificata ci permette di concludere correttamente quando i numeri sono uguali e quando non lo sono. Lo stesso risultato si può ottenere anche con tre if separate, ma non tutti i programmatori preferirebbero questo modo di procedere (tra i metodi semplici).

Switch

L’altra istruzione decisionale propria del linguaggio Java è lo switch. Quest’istruzione valuta una espressione al fine di calcolarne il valore e poi confronta quest’ultimo con quelli impostati con le clausole case. La sintassi la si evince da questo semplice esempio, il cui listato completo è scaricabile da qui:

int numero = (int)(Math.random()*9)+1;
switch(numero)
{

    case 1:    System.out.println("Dispari");break;
    case 2:    System.out.println("Pari");break;
    case 3:    System.out.println("Dispari");break;
    case 4:    System.out.println("Pari");break;
    case 5:    System.out.println("Dispari");break;
    case 6:    System.out.println("Pari");break;
    case 7:    System.out.println("Dispari");break;
    case 8:    System.out.println("Pari");break;
    case 9:    System.out.println("Dispari");break;
    case 10:    System.out.println("Pari");break;

}

Dopo aver generato un numero pseudo-casuale (i numeri completamente casuali nei computer non esistono mai, ricordatelo sempre) valutiamo l’espressione compresa tra parentesi tonde: in questo caso si tratta di un numero intero e lo confrontiamo con i valori che seguono la clausola case. Quando si incontra corrispondenza il programma esegue il codice che segue i due punti (:). Ogni porzione di codice comprende anche un’istruzione break che serve a fare in modo che il programma salti direttamente alla fine del blocco switch.
In realtà una trattazione approfondita di questi argomenti richiederebbe certamente molto più spazio, oltre dall’esulare dai nostri scopi. Per questo motivo, per quanto riguarda la trattazione della clausola default e dell’istruzione continue forniremo di volta in volta solo le informazioni necessarie per comprendere eventuali esempi. Il lettore non si preoccupi di questo: si tratta di istruzione realmente poco usate dai programmatori esperti; lo stesso risultato è ottenibile in maniera più semplice ed elegante.

I cicli: while, do while e for

Molto spesso nei nostri programmi abbiamo la necessità di eseguire la medesima operazione un certo numero di volte. per fare questo in Java disponiamo di tre costrutti: while, do while e for. Cominciamo subito con una premessa: in linea teorica qualunque porzione di codice realizzata con uno dei costrutti sopra citati può essere realizzata in maniera equivalente con gli altri due anche se è consigliabile usarli rispettivamente per gli scopi per i quali sono stati progettati ovvero:
1. while: implementazione di un ciclo del quale non sappiamo a priori il numero di ripetizioni ma conosciamo la condizione di terminazione; inoltre non siamo sicuri del fatto che debba eseguito almeno una volta.
2. do while: come while ma abbiamo l’assoluta certezza che il ciclo debba essere eseguito almeno un volta.
3. for: conosciamo a priori il numero di iterazioni.

Per meglio capire tutti questi aspetti, ho deciso di proporre come esempio un semplice programma che conti fino a 10 realizzandolo con i diversi metodi sopra citati. Il file compresso cicli.zip, scaricabile da qui, contiene alcuni esempi, di diversa difficoltà e debitamente commentati, sull’utilizzo dei vari cicli.

Il ciclo while ha una sintassi come la seguente

int i = 1;
while (i <= 10)
{
    System.out.println(i);
    i = i + 1;
}

Come potete vedere la parola riservata while è seguita da una condizione booleana racchiusa tra parentesi tonde e, successivamente, dal blocco di programma che deve essere eseguito. Anche in questo caso le parentesi graffe sono facoltative se si tratta di una unica istruzione. Quello che fa questa porzione di codice è semplicemente stampare a video il contenuto di i e, successivamente, il suo valore: in particolare l’operazione di incremento può essere eseguita anche in altri modi e di questo ci occuperemo meglio nel prossimo articolo. Chiariremo anche il perché in questo caso, al contrario degli altri esempi usati finora, non abbiamo usato gli apici all’interno della println. Per i nostri scopi, in questo momento, è sufficiente che sia chiara la sintassi e il modo in cui opera il ciclo while tenendo presente anche quanto detto poco fa. In particolare dovrebbe essere chiaro che il programma funziona in questo modo:

1. dichiarazione di i e assegnamento del valore 1 ad i
2. valutazione dell’espressione i<=10: se vera vai al punto tre altrimenti vai al punto 6
3. stampa di i
4. incremento di i
5. ritorno al punto 2
6. eventuali altre righe dopo il blocco while

La stessa operazione eseguita con un ciclo do while si presenta in questo modo:

i=1;
do
{
    System.out.println(i);
    i = i + 1;
}
while(i<=10);

In questo modo la valutazione dell’espressione avviene solo dopo la prima esecuzione del codice racchiuso nel blocco. In questo modo siamo sicuri che il codice verrà eseguito almeno una volta. Anche in questo caso le parentesi graffe possono essere omesse se si tratta di una sola riga di codice ma, poiché non tutti i linguaggi si comportano in questo modo per il ciclo do while, è consigliabile lasciarle per facilitare, eventualmente, l’apprendimento di un linguaggio differente.
Vediamo ora come si presenta l’esempio realizzato con un ciclo for:

for ( i=1 ; i <= 10 ; i++ )
        System.out.println(i);

Questa sintassi merita un ulteriore approfondimento. All’intero delle parentesi quadre che seguono la for potete notare tre “parti” distinte, separate da punti e virgola (;). La prima rappresenta una inizializzazione e viene eseguita in ogni caso solo una volta all’inizio del ciclo; la seconda rappresenta la condizione di terminazione che viene valutata, come nel ciclo while, prima dell’esecuzione dell’istruzione e la terza contiene un’istruzione, che generalmente viene usata come incremento ed eseguita ogni volta dopo l’esecuzione del blocco del for. Questo significa, ad esempio, che all’uscita del ciclo la i varrà 11 non 10!
Dovrebbe essere chiaro che nel nostro caso, poiché conosciamo a priori il numero di iterazioni, la soluzione più corretta sarebbe la terza la quale è anche la più compatta e, forse, la più elegante.

Cicli annidati, cicli infiniti

In molti casi può risultare conveniente inserire all’interno del blocco di programma di un ciclo uno o più cicli: ovviamente non vi è alcuna limitazione nel fare questo da parte del linguaggio ma dovete tenere conto che già la nidificazione di più di tre cicli, uno all’interno dell’altro rende il programma abbastanza pesante e di difficile interpretazione e manutenzione. E’ una buona regola di programmazione non andare oltre il valore sopra citato, a meno che non sia assolutamente necessario.

E’ inoltre responsabilità del programmatore fare in modo che il blocco di istruzioni di un ciclo venga eseguito un numero finito di volte. In caso contrario il programma entrerà in un “loop infinito” e non terminerà correttamente (in genere può essere interrotto con CTRL + C, diversamente è necessario arrestare il processo da “task maneger” o lanciare un kill per gli ambienti Linux). Per esempio, se per errore avessimo scritto:

int i = 1;
while (i <= 10)
{
     System.out.println(i);
     i = i - 1;
}

la condizione di terminazione sarebbe stata sempre valida, poiché decrementiamo la i invece di incrementarla ed il suo valore rimane sempre minore di 10. In questo caso tutte le istruzioni che seguono questo codice non verrebbero mai raggiunte. E’ importante notare che il compilatore non effettua alcun controllo sui cicli per impedire simili circostanze e, infatti, si tratta di uno degli errori più comuni ma anche di più facile soluzione (in genere l’errore è negli incrementi/decrementi o nella condizione di terminazione).

Nel prossimo articolo cercheremo di fare chiarezza su alcuni punti che sinora abbiamo sorvolato, concludendo di fatto la prima parte del nostro excursus dedicato alla conoscenza delle strutture base del linguaggio. Non preoccupatevi quindi se vi sono rimasti dei dubbi circa qualcuno degli esempi fin ora proposti: è assolutamente normale! Concentratevi, per ora, sugli aspetti fin qui trattati: ulteriori nozioni chiarificatrici arriveranno a breve.

Ti consigliamo anche

Link copiato negli appunti