Al momento stai visualizzando Il nuovo switch (espressioni switch)

Il nuovo switch (espressioni switch)

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’espressione switch non è possibile utilizzare i comandi return, break o continue.

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 file Semaforo.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 clausola default. In un’espressione switch 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.

Lascia un commento