Secondo alcuni sondaggi come quello di JetBrains, la versione 8 di Java è al momento quella più utilizzata in assoluto dagli sviluppatori di tutto il mondo, nonostante si tratti di una release del 2014. Quello che state leggendo è un articolo che fa parte di una serie intitolata “Superare Java 8”, ispirata ai contenuti del mio ultimo libro “Il nuovo Java”. Questi articoli accompagneranno passo dopo passo il lettore all’esplorazione delle più importanti caratteristiche introdotte a partire dalla versione 9. L’obiettivo è quello di far acquisire la consapevolezza di quanto è importante aggiornare le proprie conoscenze relative a Java, spiegando gli enormi vantaggi che offrono le ultime versioni del linguaggio.
In questo articolo vedremo come Oracle con Java 14 abbia ufficialmente introdotto un’evoluzione del costrutto switch
, che ora può essere utilizzato anche come espressione, ovvero può ritornare un valore. Tale novità è nota con il nome switch
expression (espressione switch
). Tuttavia l’introduzione delle espressioni switch
, ha causato anche la modifica del costrutto originale, con la definizione di una nuova parola nel vocabolario di Java (yield
), è la possibilità di usare in questo costrutto la notazione freccia, che è stata introdotta con le espressioni lambda con Java 8. Distingueremo quindi il costrutto switch
quando utilizzato come statement e come espressione, ma piuttosto di parlare solo delle espressioni switch
, pare più opportuno parlare di un costrutto rinnovato: il nuovo switch
, che ora può essere utilizzato come statement o come espressione.
Per avere un quadro della situazione più chiaro, descriviamo anche brevemente la storia del costrutto (se non siete interessati potete anche saltare il prossimo paragrafo).
Perché switch
è stato aggiornato
Il costrutto switch
, come tutti i costrutti di Java e l’operatore ternario, è stato ereditato dal linguaggio C sin dalla prima versione di Java (fa eccezione il ciclo for
migliorato che è stato introdotto con Java 5).
Tale costrutto, ha delle caratteristiche che ben si adattavano alla creazione dei tipici programmi che venivano sviluppati una volta con questo linguaggio, per esempio i parser e codificatori binari. Proiettato all’interno della programmazione Java però, è sempre stato visto dai programmatori come un costrutto secondario. Con l’avvento delle enumerazioni in Java, switch
ha assunto maggiore rilevanza, ma non abbastanza per essere considerato uno dei costrutti preferiti dagli sviluppatori. La sintassi è rimasta verbosa, complessa e molto diversa dagli altri costrutti. La tecnica del fall through è sempre complicata da gestire, e spesso è involontariamente causa di bug. Lo scope delle variabili locali a livello di costrutto continua ad essere scomodo. Per queste ed altre ragioni, dopo oltre 20 anni si è sentita l’esigenza di aggiornare il costrutto con nuove caratteristiche. Infatti, la scelta di Oracle di accelerare il processo di sviluppo di Java attraverso il rilascio semestrale delle nuove versioni, ben si adatta al processo di aggiornamento del linguaggio, basato sui feedback e le richieste degli sviluppatori. La piattaforma si sta arricchendo velocemente di nuove caratteristiche molto interessanti, dopo alcuni anni in cui il suo sviluppo aveva subito un evidente rallentamento, dovuto in parte al passaggio di consegne, e conseguenti necessità di prendere nuove direzioni tra Sun Microsystems e Oracle. Il nuovo ciclo di sviluppo di Java basato sulle proposte degli sviluppatori, sta dando linfa vitale ad un linguaggio che è sempre più moderno. In particolare, con la versione 12, è stata anche inaugurato un nuovo modo di introdurre una nuova caratteristica nel linguaggio: le feature preview (in italiano caratteristica in anteprima). In pratica, un nuovo costrutto (l’espressione switch
), è diventato utilizzabile in anteprima nel JDK 12. Per poter usufruire del nuovo costrutto, bisognava specificare durante la compilazione e al runtime delle particolari opzioni. In questo modo, gli sviluppatori hanno avuto la possibilità di testare le espressioni switch
, e di restituire ad Oracle fondamentali feedback.
Infatti, nella versione 13 di Java l’implementazione dell’espressioni switch
è stata riproposta con delle modifiche. In particolare, è stata introdotta la parola yield
, proprio grazie ai feedback degli sviluppatori. Nella versione 14 quindi, le espressioni switch
sono state confermate senza ulteriori modifiche e sono state ufficialmente dichiarate come nuove caratteristiche del linguaggio. Quindi oggi, non è più necessario specificare le opzioni per abilitare le feature preview per utilizzare le espressioni switch
, sono a tutti gli effetti un nuovo costrutto della programmazione Java.
Il nuovo switch
e la notazione freccia
All’interno del costrutto switch
, è ora possibile utilizzare la notazione freccia (in inglese arrow), ovvero il simbolo costituito dai caratteri ->
che già utilizziamo nelle espressioni lambda. Esso può seguire la parola chiave case
al posto del simbolo “:
”. Supponiamo di avere la seguente enumerazione che definisce i colori per un semaforo:
public enum Colore { VERDE, GIALLO, ROSSO; }
e poi consideriamo la seguente classe che rappresenta un semaforo:
public class Semaforo { public void cambiaColore(Colore colore) { switch(colore) { case VERDE: accendiLuceVerde(); break; case GIALLO: accendiLuceGialla(); break; case ROSSO: accendiLuceRossa(); break; } } // resto del codice omesso }
Ora possiamo riscrivere la classe precedente nel seguente modo:
public class Semaforo { public void cambiaColore(Colore colore) { switch(colore) { case VERDE -> accendiLuceVerde(); case GIALLO -> accendiLuceGialla(); case ROSSO -> accendiLuceRossa(); } } // resto del codice omesso }
Nel metodo cambiaColore
abbiamo utilizzato un costrutto switch
con notazione freccia, che per ogni case
esegue uno statement. Si noti che non c’è stato bisogno di utilizzare la parola chiave break
. Quindi la sintassi basata sulla notazione freccia, rende il nostro codice meno verboso, più chiaro e moderno.
Notare che per specificare più istruzioni dopo la notazione freccia, bisogna includerle in una coppia di parentesi graffe. Per esempio:
switch(colore) { case VERDE -> { accendiLuceVerde(); camera.stopRegistrazione(); } // resto del codice omesso
Il vecchio switch
e la tecnica del fall through
Consideriamo il seguente snippet che fa uso della vecchia sintassi del costrutto switch
, dell’enumerazione Month
del package del java.time
, e della tecnica del fall through:
Month month = getMonth(); String season; switch (month) { case DECEMBER: case JANUARY: case FEBRUARY: season = "winter"; break; case MARCH: case APRIL: case MAY: season = "spring"; break; case JUNE: case JULY: case AUGUST: season = "summer"; break; case SEPTEMBER: case OCTOBER: case NOVEMBER: season = "autumn"; break; default: season = "not identifiable"; break; }
Il costrutto risulta verboso, poco elegante e poco leggibile, inoltre la sintassi della tecnica del fall through porta facilmente a commettere errori. Infatti essa di basa sull’omissione dello statement break
che causa l’esecuzione delle clausole case
seguenti, simulando quello che possiamo fare con un costrutto if
basato su espressioni booleane multiple legate da operatori OR
. Per esempio potremmo riscrivere tutto con un costrutto if
come il seguente:
Month month = getMonth(); String season; if (month == DECEMBER || month == JANUARY || month == FEBRUARY) { season = "winter"; } else if (month == MARCH || month == APRIL || month == MAY) { season = "spring"; } else if (month == JUNE || month == JULY || month == AUGUST) { season = "summer"; } else if (month == SEPTEMBER || month == OCTOBER || month == NOVEMBER) { season = "autumn"; } else { season = "not identifiable"; } }
Il nuovo switch
e la tecnica del fall through
Ora è possibile utilizzare la notazione freccia che può seguire la parola chiave case
al posto del simbolo “:
”. Con questa notazione andiamo a riscrivere l’esempio precedente:
Month month = getMonth(); String season = null; switch (month) { case DECEMBER, JANUARY, FEBRUARY -> season = "winter"; case MARCH, APRIL, MAY -> season = "spring"; case JUNE, JULY, AUGUST -> season = "summer"; case SEPTEMBER, OCTOBER, NOVEMBER -> season = "autumn"; }
Si noti innanzitutto che i vari case
possono dichiarare più label separate da virgole. In questo modo non c’è necessità di utilizzare il fall through per far eseguire le stesse istruzioni a case
diversi, anzi, questa sintassi ci impedisce di utilizzare il fall through. Ovviamente non c’è stato bisogno di utilizzare la parola chiave break
. E la sintassi è indubbiamente più concisa ed elegante, anche rispetto alla versione che fa uso del costrutto if
.
Espressione switch
L’espressione switch
evolve il costrutto switch
che effettivamente soffre di diversi difetti. Per esempio, dimenticare un break significa causare un fall through involontario. Inoltre gli scenari di applicabilità sono limitati rispetto ad un classico if
, e la sintassi risulta piuttosto verbosa. Per queste ed altre ragioni lo switch
è un costrutto relativamente poco utilizzato.
Ricordiamo che con il termine espressione, intendiamo un’istruzione (come un valore literal, un’invocazione di metodo, un’operazione, etc.) che ritorna un valore. Quindi un’espressione switch
è un costrutto che ritorna un valore. Riscriviamo quindi l’esempio precedente che faceva uso della notazione freccia con un’espressione switch
:
Month month = getMonth(); String season = switch(month) { case DECEMBER, JANUARY, FEBRUARY -> "winter"; case MARCH, APRIL, MAY -> "spring"; case JUNE, JULY, AUGUST -> "summer"; case SEPTEMBER, OCTOBER, NOVEMBER -> "autumn"; };
Ci sono piccole ma importanti differenze rispetto all’esempio dello switch
usato come statement visto nel paragrafo precedente. La differenza principale, riguarda il fatto che questa espressione switch
, ritorna un valore che viene assegnato direttamente alla variabile season
. Il valore ritornato è il literal puntato dalla notazione freccia. Ricordiamo che nell’esempio del precedente paragrafo alla notazione freccia ogni volta seguiva l’assegnazione del literal alla variabile season
. Con l’espressione switch
invece possiamo notare una sintassi ancora meno verbosa che ci evita di riscrivere le assegnazioni. Infine notiamo che, trattandosi di un’espressione, deve terminare con un simbolo di punto e virgola.
La parola yield
La notazione freccia quindi può puntare ad un’espressione che nel nostro esempio era un literal, ma può puntare anche ad un blocco di codice che magari contiene diverse istruzioni. In questo caso, per ritornare un valore dal blocco di codice possiamo utilizzare la parola yield
. Con essa possiamo specificare un valore da restituire. Per esempio, la seguente espressione switch
è equivalente a quella presentata nell’esempio precedente:
var season = switch(month) { case DECEMBER, JANUARY, FEBRUARY -> { String value = "winter" ; yield value; } case MARCH, APRIL, MAY -> "spring" ; case JUNE, JULY, AUGUST -> { String value = "summer" ; yield value; } case SEPTEMBER, OCTOBER, NOVEMBER -> "autumn" ; };
Si noti, che come espressioni abbiamo usato dei semplici literals (spring e autumn), mentre nei blocchi di codice abbiamo usato la nuova sintassi che fa uso dell’istruzione yield
, che ci permette di restituire un valore. In pratica, un’istruzione yield
in un’espressione switch
, ha lo stesso ruolo che un’istruzione return
in un metodo.
Da un’espressione
switch
, è possibile uscire solo con un valore, oppure lanciando un’eccezione. Questo significa che all’interno di un’espressioneswitch
non è possibile utilizzare i comandireturn
,break
ocontinue
.
CURIOSITÀ: nella versione 12 di Java, quando le espressioni switch
furono introdotte come feature preview, al posto della parola yield
, si utilizzava la parola break
. Solo nella versione 13 quest’ultima fu sostituita dalla parola yield
. Infatti, i feedback per questa feature preview giudicarono l’utilizzo del break
fuorviante, in quanto già usato in altri contesti.
ALTRA CURIOSITÀ: Si noti che abbiamo utilizzato anche la parola var
, e che nonostante il compilatore solitamente si basi sulla parte sinistra dell’assegnazione (LHS) per validare la correttezza di un’espressione switch
, quando utilizziamo la parola var
invece si basa sulla parte destra dell’assegnazione (RHS). Per maggiori informazioni potete sulla parola var
potete leggere questo articolo.
Freccia vs due punti
Anche con lo switch
usato come espressione, in realtà è ancora possibile utilizzare il fall through. Infatti, esiste una sintassi alternativa a quella che abbiamo visto nel primo esempio, dove la notazione freccia ->
“indicava” il valore da restituire (che può essere definito da un’espressione, o da un blocco di codice). In realtà, possiamo ancora sostituire la notazione di freccia ->
, con la notazione che si usava con lo switch
ordinario (ovvero i due punti “:
“). Inoltre abbiamo visto che l’istruzione yield
in un’espressione switch
può specificare a seguire il valore da restituire:
String season = switch(month) { case DECEMBER, JANUARY, FEBRUARY: yield "winter"; case MARCH, APRIL, MAY: yield "spring"; case JUNE, JULY, AUGUST: yield "summer"; case SEPTEMBER, OCTOBER, NOVEMBER: yield "autumn"; };
Con questa sintassi, lo yield
può essere usato anche al di fuori di un blocco di codice, ed è l’unico modo per far ritornare un valore. Infatti, solo con la notazione freccia è possibile ritornare un literal direttamente. La differenza più importante tra i due tipi di sintassi però, è che con quest’ultima è possibile ancora utilizzare il fall through (anche se non se ne avverte l’esigenza). Infatti, la seguente sintassi è perfettamente valida:
String season = switch(month) { case DECEMBER: case JANUARY: case FEBRUARY: yield "winter"; case MARCH, APRIL, MAY: yield "spring"; case JUNE, JULY, AUGUST: yield "summer"; case SEPTEMBER, OCTOBER, NOVEMBER: yield "autumn"; };
Ricordiamo che abbiamo già visto nel paragrafo precedente, che anche con la sintassi che fa uso della notazione freccia ->
è possibile utilizzare l’istruzione yield
, ma solo all’interno di blocchi di codice. Per esempio questo codice è valido ed equivalente agli altri esempi visti:
String season = switch(month) { case DECEMBER, JANUARY, FEBRUARY -> {yield "winter";} case MARCH, APRIL, MAY -> {yield "spring";} case JUNE, JULY, AUGUST -> {yield "summer";} case SEPTEMBER, OCTOBER, NOVEMBER -> {yield "autumn"}; };
Non è possibile però, mischiare le due notazioni (notazione freccia e notazione due punti) nello stesso costrutto. Per esempio:
String season = switch(month) { case DECEMBER, JANUARY, FEBRUARY: yield "winter"; case MARCH, APRIL, MAY: yield "spring"; case JUNE, JULY, AUGUST: yield "summer"; case SEPTEMBER, OCTOBER, NOVEMBER -> "autumn"; };
causerebbe il seguente errore di compilazione:
error: different case kinds used in the switch case SEPTEMBER, OCTOBER, NOVEMBER -> "autumn" ; ^ 1 error
Exhaustiveness (esaustività)
Un’espressione switch
, ha tra le sue caratteristiche quella della exhaustiveness (in italiano esaustività). Ciò significa che il compilatore non accetterà situazioni in cui è assente una possibile clausola case
. Per esempio, modifichiamo la classe Semaforo definita precedentemente, in modo tale che abbia una variabile stato, che viene settata mediante un’espressione switch
. Volontariamente però, non inseriamo il case Rosso
:
public class Semaforo { public String stato; public void cambiaColore(Colore colore) { stato = switch(colore) { case VERDE -> "La luce è verde"; case GIALLO -> "La luce è gialla"; // case ROSSO -> "La luce è rossa"; }; } }
otterremmo il seguente errore in compilazione:
javac Semaforo.java Semaforo.java:4: error: the switch expression does not cover all possible input values stato = switch(colore) { ^
che ci avverte che non tutti i casi sono stati contemplati dal costrutto.
Se avessimo usato lo switch
come statement, il codice sarebbe stato compilabile. Quindi l’exhaustiveness è una caratteristica solo delle espressioni switch
.
Per far compilare il file precedente, basterebbe riabilitare il case ROSSO
decommentando la relativa riga.
Si noti però, che l’errore di cui sopra, verrà segnalato dal compilatore solo perché stiamo usando un’enumerazione (
Colore
). Infatti, al momento della compilazione del fileSemaforo.java
, il compilatore può controllare l’enumerazione per valutare quali sono tutti i suoi elementi. Questo però non sarebbe stato possibile se al posto di un’enumerazione avessimo avuto una stringa, un intero o un tipo wrapper. Infatti in questi casi, non abbiamo un numero finito di valori da poter assegnare, e quindi l’unico modo per coprire tutti i casi è aggiungere una clausoladefault
. In un’espressioneswitch
quindi, bisognerebbe sempre utilizzare la clausola default, tranne nel caso di utilizzo di un’enumerazione come valore da testare (vedi prossimo paragrafo).
Clausola default
ed esaustività
Facendo riferimento all’esempio precedente, ovviamente nessuno ci vieta di utilizzare una clausola default
in luogo del case ROSSO
mancante, ma tale soluzione, solo nel caso delle enumerazioni, potrebbe risultare dannosa! Potrebbe creare problemi anche nel caso volessimo aggiungere entrambe le clausole (case ROSSO
e default
) come di seguito:
public void cambiaColore(Colore colore) { stato = switch(colore) { case VERDE-> "La luce è verde"; case GIALLO -> "La luce è gialla"; case ROSSO -> "La luce è rossa"; default -> "Caso imprevisto"; }; }
Infatti, supponiamo che l’enumerazione Colore
si evolva per definire il colore NERO
, che servirà per gestire le situazioni in cui il semaforo è spento:
public enum Colore { VERDE, GIALLO, ROSSO, NERO; }
Quando compileremo, la clausola default
impedirebbe al compilatore di avvertirci che non stiamo coprendo tutti i casi possibili e il problema lo scopriremo solo al runtime. Lanciando la seguente classe di test, infatti:
public class TestSemaforo { public static void main(String args[]) { Semaforo semaforo = new Semaforo(); semaforo.cambiaColore(Colore.ROSSO); semaforo.stampaStato(); semaforo.cambiaColore(Colore.GIALLO); semaforo.stampaStato(); semaforo.cambiaColore(Colore.VERDE); semaforo.stampaStato(); semaforo.cambiaColore(Colore2.NERO); semaforo.stampaStato(); } }
otterremo il seguente output:
java TestSemaforo La luce è rossa La luce è gialla La luce è verde Caso imprevisto
Conclusioni
Con Java 14 abbiamo un costrutto switch
rinnovato e molto più interessante rispetto a quello che abbiamo usato per tanti anni. In particolare la notazione freccia, e la possibilità di raggruppare i valori di più case
, separandoli con virgole, ci garantisce minore verbosità e maggiore robustezza del nostro codice. Possiamo ancora usare la notazione classica dei due punti (e di conseguenza la tecnica del fall through) per ragioni di compatibilità con il codice scritto con le versioni precedenti di Java, tuttavia probabilmente la vecchia sintassi sarà probabilmente del tutto abbandonata in futuro. La parola yield
permette di ritornare un valore nelle espressioni switch
, come la parola return
permette di ritornare un valore nei metodi. Infine, il concetto di esaustività delle espressioni switch
, ci garantisce un controllo a livello di compilazione della completezza della definizione quando usiamo le enumerazioni. Il nuovo switch
rappresenta quindi un altro fondamentale motivo per superare Java 8.
Note dell’autore
Anche ignorando la maggiore sicurezza che offrono le ultime versioni del JDK, esistono tantissimi motivi per aggiornare le proprie conoscenze di Java, o quantomeno le proprie installazioni del runtime di Java. Il mio ultimo libro “Il nuovo Java”, a cui si ispira la serie “Superare Java 8”, contiene tutte le informazioni per poter imparare Java da zero, ed utilizza un metodo didattico ben collaudato e perfezionato in 20 anni di esperienza, che rende l’apprendimento semplice e appassionante. Inoltre è strutturato per approfondire gli argomenti ed avere una conoscenza superiore che può fare la differenza per la vostra carriera lavorativa.
Questo articolo è ispirato principalmente ad alcuni paragrafi del capitolo 3 del libro “Il nuovo Java”.
Per maggiori informazioni visitate https://www.nuovojava.it.