Istruzioni case con freccia in Java

Concetti Chiave
  • Le istruzioni case con freccia in Java offrono un modo più conciso e leggibile per gestire le condizioni nei blocchi switch.
  • Queste istruzioni eliminano la necessità di utilizzare l'istruzione break, riducendo il rischio di errori di fall-through.
  • Le istruzioni case con freccia possono essere utilizzate sia in espressioni switch che in istruzioni switch, offrendo flessibilità nel codice.
  • L'uso di blocchi di codice con freccia consente di eseguire più operazioni all'interno di un singolo case, migliorando la chiarezza del codice.

L'uso della freccia nei case

Sebbene l'uso di yield sia un modo perfettamente valido per specificare un valore per un'espressione switch, non è l'unico modo per farlo. In molte situazioni, un modo più semplice per fornire un valore è attraverso l'uso di una nuova forma del case che sostituisce -> (una freccia) ai due punti in un case.

Per esempio, questa riga:

case 'X': // ...

può essere riscritta usando la freccia così:

case 'X' -> // ...

Per evitare confusione, in questa discussione ci riferiremo a un case con una freccia come caso freccia e alla forma tradizionale basata sui due punti come caso due punti. Sebbene entrambe le forme corrisponderanno al carattere X, l'azione precisa di ogni stile di istruzione case differisce in tre modi molto importanti:

  1. Primo, un case freccia non continua al case successivo. Quindi, non c'è bisogno di usare break.

    L'esecuzione semplicemente termina alla fine di un case freccia. Sebbene la natura di continuazione di un case tradizionale basato sui due punti sia sempre stata parte di Java, la continuazione è stata criticata perché può essere una fonte di bug, come quando il programmatore dimentica di aggiungere un'istruzione break per prevenire la continuazione quando la continuazione non è desiderata. Il case freccia evita questa situazione.

  2. Secondo, il case freccia fornisce un modo "abbreviato" per fornire un valore quando usato in un'espressione switch. Per questa ragione, il case freccia è spesso usato nelle espressioni switch.

  3. Terzo, il target di un case freccia deve essere un'espressione oppure un blocco oppure lanciare un'eccezione.

    Non può essere una sequenza di istruzioni, come è consentito con un case tradizionale. Quindi, il caso freccia avrà una di queste forme generali:

    case costante -> espressione;
    case costante -> { blocco-di-istruzioni }
    case costante -> throw eccezione;
    

    Naturalmente, le prime due forme rappresentano gli usi primari.

Probabilmente, l'uso più comune di un case freccia è in un'espressione switch, e il target più comune del case freccia è un'espressione. Quindi, è qui che inizieremo. Quando il target di un case freccia è un'espressione, il valore di quell'espressione diventa il valore dell'espressione switch quando quel case viene abbinato. Quindi, fornisce un'alternativa molto efficiente all'istruzione yield in molte situazioni. Per esempio, ecco il primo case nell'esempio delle lezioni precedenti riscritto per usare un case freccia:

case 1000, 1205, 8900 -> 1;

Qui, il valore dell'espressione (che è 1) diventa automaticamente il valore prodotto dallo switch quando questo case viene abbinato. In altre parole, l'espressione diventa il valore prodotto dallo switch. Si noti che questa istruzione è piuttosto compatta, ma esprime chiaramente l'intento di fornire un valore.

Proviamo a riscrivere completamente il programma delle lezioni precedenti usando un case freccia. Per farlo, dobbiamo riscrivere ogni case per usare un case freccia e rimuovere le istruzioni break. Inoltre, poiché non c'è bisogno di un'istruzione yield quando si usa un case freccia, possiamo rimuovere anche quelle. Ecco il programma completo:

// Usa una switch expression con case freccia
class SwitchFreccia {

    public static void main(String[] args) {

        // Inizializza il codice evento
        int codiceEvento = 6010;

        // Usa una switch expression per determinare il livello di priorità
        int livelloPriorita = switch (codiceEvento) {
            case 1000, 1205, 8900 -> 1;
            case 2000, 6010, 9128 -> 2;
            case 1002, 7023, 9300 -> 3;
            default -> 0;
        };

        // Stampa il livello di priorità
        System.out.println("Livello di priorità per il codice evento " +
                           codiceEvento +
                           " è " + livelloPriorita);
    }

}

Questo produce lo stesso output di prima. Guardando il codice, è facile vedere perché questa forma del case freccia è così appropriata per molti tipi di espressioni switch. È compatta ed elimina la necessità di un'istruzione yield separata. Poiché il case freccia non continua, non c'è bisogno di un'istruzione break. Ogni case termina producendo il valore della sua espressione. Inoltre, se confrontiamo questa versione finale dello switch con lo switch tradizionale originale mostrato all'inizio di questa discussione, è facilmente evidente quanto sia snella ed espressiva questa versione. In combinazione, i miglioramenti dello switch offrono un modo davvero impressionante per migliorare la chiarezza e la resilienza del codice.

Approfondimenti sul case con freccia

Il case con freccia fornisce una flessibilità considerevole. Prima di tutto, quando si usa la sua forma di espressione, l'espressione può essere di qualsiasi tipo. Per esempio, la seguente è un'istruzione case valida:

case -1 > ottieniCodiceErrore();

Qui, il risultato della chiamata a ottieniCodiceErrore() diventa il valore dell'espressione switch che la racchiude. Ecco un altro esempio:

case 0 -> completamentoNormale = true;

In questo caso, il risultato dell'assegnazione, che è true, diventa il valore prodotto. Il punto chiave è che qualsiasi espressione valida può essere usata come target del case con freccia purché sia compatibile con il tipo richiesto dal switch.

Come menzionato, il target del -> può anche essere un blocco di codice. Bisognerà usare un blocco come target di un case con freccia ogni volta che si ha bisogno di più di una singola espressione. Per esempio, ogni case in questa versione del programma del codice evento imposta il valore di una variabile boolean chiamata fermatiOra per indicare se è richiesta una terminazione immediata e poi produce il livello di priorità.

// Usa case con freccia e blocchi
class CaseFrecciaBlocco {
    public static void main(String[] args) {

        // Flag che indica se il processo deve fermarsi immediatamente
        boolean fermatiOra;
        int codiceEvento = 9300;

        // Usa blocchi di codice con una freccia. Ancora, si noti
        // che nessuna istruzione break è richiesta (o permessa)
        // per prevenire il fall through. Poiché il target di una
        // freccia è un blocco, yield deve essere usato per fornire un valore.
        int livelloPriorita = switch (codiceEvento) {
            case 1000, 1205, 8900 -> {
                fermatiOra = false;
                System.out.println("Allerta");
                yield 1;
            }
            case 2000, 6010, 9128 -> {
                fermatiOra = false;
                System.out.println("Avviso");
                yield 2;
            }
            case 1002, 7023, 9300 -> {
                fermatiOra = true;
                System.out.println("Pericolo");
                yield 3;
            }
            default -> {
                fermatiOra = false;
                System.out.println("Normale.");
                yield 0;
            }
        };

        System.out.println("Livello di priorità per il codice evento " + codiceEvento +
                           " è " + livelloPriorita);

        if (fermatiOra) {
            System.out.println("Stop richiesto.");
        }
    }

}

Di seguito è riportato l'output di questo programma:

Pericolo
Livello di priorità per il codice evento 9300 è 3
Stop richiesto.

Come mostra questo esempio, quando si usa un blocco, si deve usare yield per fornire un valore a un'espressione switch. Inoltre, anche se vengono usati target a blocco, ogni percorso attraverso l'espressione switch deve ancora fornire un valore.

Anche se il programma precedente fornisce una semplice illustrazione di un target a blocco di un case con freccia, solleva anche una domanda interessante. Si noti che ogni case nello switch imposta il valore di due variabili. La prima è livelloPriorita, che è il valore prodotto. La seconda è fermatiOra. C'è un modo per un'espressione switch di produrre più di un valore? In senso diretto, la risposta è "no" perché solo un valore può essere prodotto dallo switch. Tuttavia, è possibile incapsulare due o più valori all'interno di una classe e produrre un oggetto di quella classe.

A partire dal JDK 16, Java fornisce un modo particolarmente snello ed efficiente per compiere questo: i record. Studieremo in dettaglio i record nelle prossime lezioni. Per il momento basta sapere che un record aggrega due o più valori in una singola unità logica. Come si relaziona a questo esempio, un record potrebbe contenere sia i valori livelloPriorita che fermatiOra, e questo record potrebbe essere prodotto dallo switch come un'unità. Quindi, un record offre un modo conveniente per uno switch di produrre più di un singolo valore.

Anche se il case con freccia è molto utile in un'espressione switch, è importante enfatizzare che non è limitato a quell'uso. Il case con freccia può anche essere usato in un'istruzione switch, che permette di scrivere switch in cui non può verificarsi alcun fall-through del case. In questa situazione, nessuna istruzione yield è richiesta (o permessa), e nessun valore è prodotto dallo switch. In sostanza, funziona molto come uno switch tradizionale ma senza il fall-through. Ecco un esempio:

// Usa case frecce con un'istruzione switch
class IstruzioneSwitchConFrecce {

    public static void main(String[] args) {
        int su = 0;
        int giù = 0;
        int sinistra = 0;
        int destra = 0;
        char direzione = 'R';
        // Usa case frecce con un'istruzione switch. Nota che
        // nessun valore è prodotto.
        switch (direzione) {
            case 'L' -> {
                System.out.println("Gira a Sinistra");
                sinistra++;
            }
            case 'R' -> {
                System.out.println("Gira a Destra");
                destra++;
            }
            case 'U' -> {
                System.out.println("Muove Su");
                su++;
            }
            case 'D' -> {
                System.out.println("Muove Giù");
                giù++;
            }
        }
        System.out.println(destra);
    }

}

In questo programma, lo switch è un'istruzione, non un'espressione. Questo è dovuto a due ragioni. Prima, nessun valore è prodotto. Secondo, non è esaustivo perché nessuna istruzione default è inclusa. (Si ricordi che le espressioni switch devono essere esaustive, ma non le istruzioni switch.) Si noti, tuttavia, che poiché non si verifica fall-through con un case con freccia, nessuna istruzione break è necessaria. Come punto di interesse, poiché ogni case incrementa il valore di una variabile diversa, non sarebbe possibile trasformare questo switch in un'espressione. Quale valore produrrebbe? Tutti e quattro i case incrementano una variabile diversa.

Un ultimo punto: non si può mescolare case con freccia con case tradizionali con due punti nello stesso switch. Bisogna scegliere uno o l'altro. Per esempio, questa sequenza non è valida:

// ERRORE
// Questo codice non compila perché mescola stili di case.
switch(direzione) {
    case 'L' -> {
        System.out.println("Girando a Sinistra");
        sinistra++;
    }
    case 'R' : // Sbagliato!
        System.out.println("Girando a Destra");
        destra++;
        break; 
    case 'U' -> {
        System.out.println("Muovendosi Su");
        su++;
    }
    case 'D' -> {
        System.out.println("Muovendosi Giù");
        giù++;
    }
}

Un altro esempio di espressione switch

Per concludere questa panoramica sui miglioramenti di switch, viene presentato un altro esempio. Utilizza un'espressione switch per determinare se una lettera è una vocale della lingua inglese. Fa uso di tutte le nuove funzionalità di switch. Si presti particolare attenzione al modo in cui viene gestita la Y. In inglese, Y può essere una vocale o una consonante. Il programma permette di specificare in che modo si vuole interpretare la Y attraverso il modo in cui è impostata la variabile yEVocale. Per gestire questo caso speciale, viene utilizzato un blocco come destinazione della freccia ->.

// Usa un'espressione switch per determinare
// se un carattere è una vocale inglese.
// Si noti l'uso di un blocco come destinazione di un caso freccia per Y.
class Vocali {

    public static void main(String[] args) { 
        // Questo flag indica se Y è considerata una vocale.
        // Se è impostato su true, Y è una vocale.
        // Se è impostato su false, Y è una consonante.
        boolean yVocale = true; 

        char ch = 'Y';
        boolean vocale = switch(ch) {
            case 'a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U' -> true;
            case 'y', 'Y' -> {
                if(yVocale)
                    yield true;
                else
                    yield false;
            }
            default -> false;
        };

        if(vocale) {
            System.out.println(ch + " è una vocale.");
        }
    }

}

Come esperimento, si provi a riscrivere questo programma utilizzando uno switch tradizionale. Come si vedrà, si otterrà una versione molto più lunga e meno gestibile. I nuovi miglioramenti di switch spesso forniscono un approccio superiore.