7. Flusso di controllo
Indice
- 7.1 L’istruzione if
- 7.2 L’istruzione Switch & la Switch Expression
- 7.3 Due forme di switch: switch Statement vs switch Expression
- 7.4 Gestione di null
Il flusso di controllo in Java si riferisce all’ordine in cui le singole istruzioni, i comandi o le chiamate a metodo vengono eseguiti durante l’esecuzione del programma.
Per impostazione predefinita, le istruzioni vengono eseguite sequenzialmente dall’alto verso il basso, ma le istruzioni di controllo del flusso consentono al programma di prendere decisioni, ripetere azioni o diramare i percorsi di esecuzione in base a condizioni.
Java fornisce tre categorie principali di costrutti di controllo del flusso:
- Istruzioni decisionali —
if,if-else,switch - Istruzioni di iterazione —
for,while,do-whilee ilforavanzato - Istruzioni di diramazione —
break,continueereturn
Tip
Comprendere il flusso di controllo è essenziale per vedere come i dati si muovono all’interno del programma e come ogni decisione logica viene valutata passo dopo passo.
7.1 L’istruzione if
L’istruzione if è una struttura condizionale di controllo del flusso che esegue un blocco di codice solo se una specifica espressione booleana viene valutata come true.
L’istruzione consente al programma di prendere decisioni a runtime.
Sintassi:
if (condition) {
// eseguito solo quando la condizione è true
}
Una clausola else opzionale gestisce il percorso alternativo:
if (score >= 60) {
System.out.println("Passed");
} else {
System.out.println("Failed");
}
Più condizioni possono essere concatenate usando else if:
if (grade >= 90) {
System.out.println("A");
} else if (grade >= 80) {
System.out.println("B");
} else if (grade >= 70) {
System.out.println("C");
} else {
System.out.println("D or below");
}
Note
La condizione di if deve essere valutata come boolean; i tipi numerici o gli oggetti non possono essere usati direttamente come condizioni.
Le parentesi graffe {} sono opzionali per singole istruzioni ma sono fortemente consigliate per prevenire sottili errori di logica.
Una catena if-else viene valutata dall’alto verso il basso, e viene eseguito solo il primo ramo con una condizione valutata come true.
7.2 L’istruzione switch & la switch Expression
Il costrutto switch è una struttura di controllo del flusso che seleziona un ramo tra più alternative in base al valore di un’espressione (il selector).
Rispetto a lunghe catene di if-else-if, uno switch:
- È spesso più facile da leggere quando si testano molti valori discreti (costanti, enum, stringhe).
- Può essere più sicuro e più conciso quando usato come espressione switch
perché:
- Produce un valore.
- Il compilatore può imporre esaustività e coerenza di tipo.
Java 21 supporta:
- La
switchclassica come istruzione (solo controllo del flusso). - La
switchcome Expression (produce un risultato). - Pattern matching dentro
switch, inclusi type pattern e guard.
Entrambe le forme di switch condividono le stesse regole riguardanti il selector (la variabile target dello switch) e i valori case accettabili.
7.2.1 La variable target dello switch può essere
| Control Variable type |
|---|
byte / Byte |
short / Short |
char / Character |
int / Integer |
String |
Enum types (selectors of an enum) |
| Any reference type (with pattern matching) |
var (if it resolves to one of the allowed types) |
Warning
Non consentiti come type selector per switch:
booleanlongfloatdouble
7.2.2 Valori case accettabili
Per uno switch non-pattern, ogni etichetta case deve essere una costante a compile-time compatibile con il tipo del selector.
Sono consentite, come etichette case:
- Letterali come
0,'A',"ON". - Costanti enum, ad es.
REDoColor.GREEN. - Variabili costanti final (costanti a compile-time).
Una costante a compile-time:
- Deve essere dichiarata con
finale inizializzata nella stessa istruzione. - Il suo inizializzatore deve a sua volta essere un’espressione costante (tipicamente usando letterali e altre costanti a compile-time).
7.2.3 Compatibilità di tipo tra selector e case
Il tipo del selector e ogni etichetta case devono essere compatibili:
- Le costanti numeriche dei case devono essere entro l’intervallo del tipo del selector.
- Per un selector
enum, le etichettecasedevono essere costanti di quell’enum. - Per un selector
String, le etichettecasedevono essere costanti stringa.
7.2.4 Pattern Matching nello Switch
Lo switch in Java 21 supporta il pattern matching, includendo:
- Type pattern:
case String s - Guarded pattern:
case String s when s.length() > 3 - Null pattern:
case null
Esempio:
String describe(Object o) {
return switch (o) {
case null -> "null";
case Integer i -> "int " + i;
case String s when s.isEmpty() -> "empty string";
case String s -> "string (" + s.length() + ")";
default -> "other";
};
}
Punti chiave:
- Ogni pattern introduce una
pattern variable(comeios). - Le pattern variable sono in scope solo all’interno del proprio
ramo(o dei percorsi in cui è noto che il pattern corrisponde). - L’ordine è importante a causa della dominanza: i pattern più specifici devono precedere quelli più generali.
7.2.4.1 Nomi delle variabili e scope tra i rami
Con il pattern matching, la variabile di pattern esiste solo nello scope del ramo in cui è definita.
Questo significa che puoi riutilizzare lo stesso nome di variabile in diversi rami case senza che i nomi entrino in conflitto.
- Esempio:
switch (o) {
case String str -> System.out.println(str.length());
case CharSequence str -> System.out.println(str.charAt(0));
default -> { }
}
Note
Quest’ultimo esempio non restituisce un valore, quindi è in realtà una istruzione switch, non una switch Expression.
7.2.4.2 Ordinamento, dominanza ed esaustività negli switch con pattern
Quando si gestisce il pattern matching, l’ordinamento dei rami è cruciale a causa della dominanza e del potenziale codice irraggiungibile.
Un pattern più generale non deve apparire prima di uno più specifico, altrimenti quello specifico diventa irraggiungibile.
- Esempio (ramo irraggiungibile):
return switch (o) {
case Object obj -> "object";
case String s -> "string"; // ❌ DOES NOT COMPILE: irraggiungibile, String è già intercettata da Object
};
- Un altro esempio con una guard:
return switch (o) {
case Integer a -> "First";
case Integer a when a > 0 -> "Second"; // ❌ DOES NOT COMPILE: irraggiungibile, il primo case intercetta tutti gli Integers
// ...
};
Quando si usa il pattern matching, gli switch devono essere esaustivi; cioè, devono gestire tutti i possibili valori del selector.
Questo può essere ottenuto tramite:
- Fornire un case
defaultche gestisce tutti i valori non corrispondenti a nessun altro case. -
Fornire una clausola
case terminalecon un tipo di pattern che corrisponde al tipo reference del selector. -
Esempio (non esaustivo):
Number number = Short.valueOf(10);
switch (number) {
case Short s -> System.out.println("A"); // ❌ DOES NOT COMPILE: non esaustivo, il selector è di tipo Number
}
Per correggere questo, puoi:
- Cambiare il tipo reference di
numberinShort(allora l’esaustività è soddisfatta dal singolo case). - Aggiungere una clausola
defaultche copre tutti i valori rimanenti. - Aggiungere una clausola
casefinale che copre il tipo della variabile selector, per esempio:
Number number = Short.valueOf(10);
switch (number) {
case Short s -> System.out.println("A");
case Number n -> System.out.println("B");
}
Warning
Il seguente esempio, che usa sia una clausola default sia una clausola finale con lo stesso tipo della variabile selector, non compila: il compilatore considera uno dei due case come sempre dominante rispetto all’altro.
Number number = Short.valueOf(10);
switch (number) {
case Short s -> System.out.println("A");
case Number n -> System.out.println("B"); // ❌ DOES NOT COMPILE: dominated by either the default or the Number pattern
default -> System.out.println("C");
}
7.3 Due forme di switch: switch Statement vs switch Expression
7.3.1 L’istruzione Switch
Una istruzione switch è usata come costrutto di controllo del flusso.
Non viene valutata, di per sé, come un valore, anche se i suoi rami possono contenere istruzioni return che ritornano dal metodo contenitore.
switch (mode) { // switch statement
case "ON":
start();
break; // prevents fall-through
case "OFF":
stop();
break;
default:
reset();
}
Punti chiave:
- Ogni clausola
caseinclude uno o più valori corrispondenti separati da virgole,. Segue un separatore, che può essere due punti:o, meno comunemente per leistruzioni, l’operatore freccia->. Infine, un’espressione o un blocco (racchiuso in{}) definisce il codice da eseguire quando si verifica una corrispondenza. Se si usa l’operatore freccia per una clausola, si deve usare per tutte le clausole in quella istruzione switch. - Il fall-through è possibile per i
casein stile "due punti" a meno che un ramo usibreak,returnothrow. Quando presente,breaktermina lo switch dopo l’esecuzione del suo case; senza di esso, l’esecuzione continua, in ordine, nei rami successivi. - Una clausola
defaultè opzionale e può apparire ovunque nell’istruzione switch. Viene eseguita se non c’è corrispondenza per i case precedenti. - Un’istruzione switch non produce un valore come nell'Expression; non puoi assegnare un’istruzione switch direttamente a una variabile.
7.3.1.1 Comportamento di Fall-Through
Con i case in stile "due punti", l’esecuzione salta all’etichetta case corrispondente.
Se non c’è un break, continua nel case successivo finché non viene incontrato un break, return o throw.
int n = 2;
switch (n) {
case 1:
System.out.println("1");
case 2:
System.out.println("2"); // printed
case 3:
System.out.println("3"); // printed (fall-through)
break;
default:
System.out.println("message default");
}
Output:
2
3
Note
Se nell’esempio precedente rimuoviamo il break sul case 3, verrà stampato anche il messaggio del ramo default.
7.3.2 L’espressione Switch
Una espressione switch produce sempre un singolo valore come suo risultato.
- Esempio:
int len = switch (s) { // switch expression
case null -> 0;
case "" -> 0;
default -> s.length();
};
Punti chiave:
- Ogni clausola
caseinclude uno o più valori corrispondenti separati da virgole,, seguiti dall’operatore freccia->. Poi un’espressione o un blocco (racchiuso in{}) definisce il risultato per quel ramo. - Quando usata con un’assegnazione o un’istruzione
return, un’espressione switch richiede un punto e virgola finale;dopo l’espressione. - Non c’è fall-through tra i rami in stile "freccia". Ogni ramo corrispondente viene eseguito esattamente una volta.
- Un’espressione switch deve essere esaustiva: tutti i possibili valori del selector devono essere coperti (tramite case espliciti e/o
default). - Il tipo del risultato deve essere coerente tra tutti i rami. Per esempio, se un ramo produce un
int, gli altri rami devono produrre valori compatibili conint.
7.3.2.1 yield nei blocchi di espressione switch
Quando un ramo di un’espressione switch usa un blocco invece di una singola espressione, devi usare yield per fornire il risultato di quel ramo.
int len = switch (s) {
case null -> 0;
default -> {
int l = s.trim().length();
System.out.println("Length: " + l);
yield l; // result of this arm
}
};
Note
yield è usato solo nelle Expressions switch.
break value; non è consentito come modo per restituire un valore da un’espressione switch.
7.3.2.2 Esaustività per le espressioni switch
Poiché un’espressione switch deve restituire un valore, deve anche essere esaustiva; in altre parole, deve gestire tutti i possibili valori del selector.
Puoi assicurare questo tramite:
- Fornire un case
default. - Per un selector enum: coprire esplicitamente tutte le costanti enum.
- Per tipi sealed o pattern switch: coprire tutti i sottotipi permessi o fornire un
default.
Esempio, esaustivo tramite default:
int val = switch (s) {
case "one" -> 1;
case "two" -> 2;
default -> 0;
};
7.4 Gestione di null
Switch classico (senza pattern)
Se l’espressione selector di uno switch classico (senza pattern matching) viene valutata come null, viene lanciata una NullPointerException a runtime.
Per evitare questo, controlla null prima di fare lo switch:
if (s == null) {
// handle null
} else {
switch (s) {
case "A" -> ...
default -> ...
}
}
Pattern switch (con case null)
Con il pattern matching, puoi gestire null direttamente dentro lo switch:
int len = switch (s) {
case null -> 0;
default -> s.length();
};
Note
Per le Expressions switch:
Se non gestisci null e il selector è null, viene lanciata una NullPointerException.
Usare case null rende lo switch esplicitamente null-safe.
Warning
Ogni volta che case null viene usato in uno switch, lo switch viene trattato come un pattern switch, e si applicano tutte le regole per i pattern switch (incluse esaustività e dominanza).