Da Java 11 a Java 17: le novità più importanti

Da Java 11 a Java 17: le novità più importanti

 

Di seguito presenteremo le più importanti novità introdotte nel linguaggio a partire dalla versione 12 alla versione 17. Ovviamente questo articolo è impostato solo su semplici esempi accompagnati da altrettanto semplici descrizioni. Per ogni argomento presentato però, viene linkato un altro articolo di approfondimento. Inoltre verranno trattate solo le novità più rilevanti che riguardano il linguaggio e che quindi impattano sul lavoro quotidiano dello sviluppatore. Non tratteremo le novità relative ad i tool, garbage collectors, librerie, migrazioni, implementazioni e deprecazioni della piattaforma Java. 

 

Switch Expressions 

Dalla versione 14 il costrutto switch è stato rivisto per essere usato come espressione (ovvero come istruzione che ritorna un valore) e introducendo una nuova sintassi basata sull’operatore freccia -> 

Per esempio oggi possiamo scrivere codice come il seguente: 

import java.time.Month; 

 

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”; 

}; 

Possiamo osservare che il costrutto ora ritorna un valore (switch expression) che viene immagazzinato nella variabile season. Inoltre la sintassi per ritornare tale valore si basa sull’operatore freccia il quale ci permette di poter evitare l’uso della parola chiave break, e di utilizzare la controversa tecnica del fall-through. 

In realtà ci sono tante altre cose da dire riguardo il nuovo switch. Per esempio, che la sintassi basata sull’operatore freccia è utilizzabile anche con lo switch nella versione tradizionale (il costrutto non è un’espressione e non ritorna un valore). Poi è stata introdotta una nuova parola chiave contestuale: yield. Inoltre, grazie alla proprietà nota come exhaustiveness, il compilatore è capace di comprendere se abbiamo coperto tutti i casi. Infatti nel nostro esempio la clausola default non si deve usare.  

Per tutti i dettagli potete consultare questo articolo d’approfondimento. 

 

Text Block 

Nella versione 15 è stata ufficializzata una importante novità per gli sviluppatori: i text block. Quando abbiamo bisogno di formattare il contenuto di una stringa per favorirne la leggibilità, siamo costretti ad utilizzare concatenazioni e caratteri di escape. Questo è particolarmente vero quando usiamo stringhe che contengono istruzioni di altri linguaggi come JSON, XML o SQL. Per esempio, consideriamo la seguente stringa JSON: 

private final static String JSON_STRING = 

    “{\n” + 

    ”    \”id\”:12345,\n” + 

    ”    \”nome\”:\”Jean\”,\n” + 

    ”    \”cognome\”:\”Valjean\”,\n” + 

    ”    \”indirizzo\”:{\n” + 

    ”        \”via\”:\”Rue Plumet\”,\n” + 

    ”        \”civico\”:7,\n” + 

    ”        \”cap\”:75015\n” + 

    ”    }\n” + 

    “}”; 

Oggi possiamo usare i Text Block che ci permettono di dichiarare stringhe che si estendono su più righe. Quindi possiamo riscrivere la stringa precedente con la seguente sintassi: 

private final static String JSON_TEXTBLOCK = “”” 

{ 

    “id”:12345, 

    “nome”:”Jean”, 

    “cognome”:”Valjean”, 

    “indirizzo”:{ 

        “via”:”Rue Plumet”, 

        “civico”:7, 

        “cap”:75015 

    } 

}”””; 

Possiamo notare che i text block sono delimitati da una sequenza di tre virgolette, il che ci permette di utilizzare al suo interno virgolette singole senza utilizzare caratteri di escape. Ma i text block sono anche delle stringhe a tutti gli effetti e quindi possiamo utilizzare tutti i metodi della classe String (altri ne sono stati aggiunti appositamente per supportare i text block). 

Anche per quanto riguarda i text block c’è tanto da approfondire. Si possono usare nuovi caratteri di escape, parametrizzare i text block, usare best practices e così via.  

Per tutti i dettagli potete consultare questo articolo d’approfondimento. 

 

Tipi Record  

La versione 16 ha introdotto ufficialmente un nuovo tipo di Java che si va ad aggiungere alle classi, le interfacce, le enumerazioni e alle annotazioni: i record 

Con un tipo record possiamo rappresentare contenitori di dati immutabili, senza creare una classe appositamente. La sintassi dei record aiuta gli sviluppatori a concentrarsi sulla progettazione di tali dati, senza perdersi nei dettagli implementativi. 

La caratteristica più evidente di un record è indubbiamente la sinteticità. Supponiamo per esempio di voler rappresentare una classe Quadro i cui oggetti sono immutabili. Prima della comparsa dei tipi record in Java, dovevamo scrivere una classe come la seguente: 

public final class Quadro { 

    private final String titolo; 

    private final String autore; 

    private final int prezzo; 

    public Quadro(String titolo, String autore, int prezzo) { 

        this.titolo = titolo; 

        this.autore = autore; 

        this.prezzo = prezzo; 

    } 

    public String getTitolo() { 

        return titolo; 

    } 

    public String getAutore() { 

        return autore; 

    } 

    public int getPrezzo() { 

        return prezzo; 

    } 

    @Override 

    public int hashCode() { 

        final int prime = 31; 

        int result = 1; 

        result = prime * result + ((autore == null) ? 0 : autore.hashCode()); 

        result = prime * result + prezzo; 

        result = prime * result + ((titolo == null) ? 0 : titolo.hashCode()); 

        return result; 

    } 

    @Override 

    public boolean equals(Object obj) { 

        if (this == obj) 

            return true; 

        if (obj == null) 

            return false; 

        if (getClass() != obj.getClass()) 

            return false; 

        Quadro other = (Quadro) obj; 

        if (autore == null) { 

            if (other.autore != null) 

                return false; 

        } else if (!autore.equals(other.autore)) 

            return false; 

        if (prezzo != other.prezzo) 

            return false; 

        if (titolo == null) { 

            if (other.titolo != null) 

                return false; 

        } else if (!titolo.equals(other.titolo)) 

            return false; 

        return true; 

    } 

    @Override 

    public String toString() { 

        return “Quadro [titolo=” + titolo + “, autore=” +  

            autore + “, prezzo=” + prezzo + “]” ; 

    } 

} 

Oggi invece possiamo semplicemente dichiarare il seguente record: 

public record Quadro(String titolo, String autore, int prezzo) { } 

Infatti tale record verrà trasformato dal compilatore in una classe come la precedente, con poche differenze: la classe generata dal compilatore estenderà la classe java.lang.Record e i metodi getter si chiameranno esattamente come i nomi delle variabili d’istanza (in questo caso titolo(), autore() e prezzo()). Quindi con una riga di codice possiamo definire l’equivalente classe immutabile generando automaticamente oltre ai metodi getter, i metodi toString(), equals(), hashcode(), il costruttore (detto costruttore canonico). 

Per approfondire potete riferirvi all’articolo di approfondimento. 

 

Sealed Classes 

La versione 17 ha introdotto una nuova caratteristica nota con il nome Sealed Classes (che in italiano possiamo tradurre come classi sigillate), ma che in realtà riguarda anche le interfacce, e che quindi preferiamo chiamare Sealed Types (ovvero tipi sigillati). Ora possiamo dichiarare classi ed interfacce imponendo alcuni limiti sulla loro estensione/implementazione. Prima dell’avvento di questa caratteristica, l’unico controllo che avevamo sull’estensibilità, consisteva nell’impedire ad una classe di essere estesa dichiarandola final (oppure dichiarando tutti i suoi costruttori privati). Una classe dichiarata con il modificatore sealed invece deve dichiarare anche quali sono le sue uniche sottoclassi. Questo permette maggiore controllo sull’ereditarietà, aprendo la via ad altre importanti caratteristiche come il pattern matching per il costrutto switch come vedremo nell’ultimo paragrafo di questo articolo.  

È stato introdotto il modificatore sealed che può essere utilizzato solo per classi ed interfacce, e la parola chiave permits che rappresenta una clausola per specificare le sottoclassi/sottointerfacce. Quindi un tipo dichiarato sealed deve specificare anche da quali classi viene esteso/implementato, di solito specificando la clausola permits. Consideriamo per esempio la seguente gerarchia: 

public sealed abstract class DiscoOttico permits CD, DVD { 

    // codice omesso 

} 

 

public final class CD extends DiscoOttico { 

    // codice omesso 

} 

 

public final class DVD extends DiscoOttico { 

    // codice omesso 

} 

In pratica la classe DiscoOttico può essere estesa solo dalle classi CD e DVD. Per definire un’altra sottoclasse, bisognerà aggiungerla alla lista definita dalla clausola permits, altrimenti il codice non sarà compilabile. Questa caratteristica incoraggia a creare gerarchie semplici e coerenti, e permette a colpo d’occhio la visualizzazione delle sottoclassi leggendo la dichiarazione della superclasse. Notare che le sottoclassi di una classe sealed devono a loro volta essere dichiarate final, sealed oppure non-sealed (la terza parola chiave contestuale introdotta per il supporto a questa caratteristica).  

Per approfondire potete riferirvi al relativo articolo di approfondimento. 

 

Pattern matching per instanceof 

Il pattern matching è una articolata caratteristica che Java sta introducendo gradualmente nel linguaggio, che ci permette tramite un predicato (test basato sul matching di un operando) di ottenere una o più variabili di binding, ovvero delle variabili il cui scope dipende dal predicato. Il pattern matching per instanceof semplifica il tipico utilizzo di questo operatore. Per esempio, supponiamo di avere una classe Punto che dichiara le variabili d’istanza x e y, potremmo definire il metodo equals nel seguente modo: 

public boolean equals(Object obj) { 

  if (obj instanceof Punto) { 

    Punto other = (Punto)obj 

    return (this.x == other.x && 

      this.y == other.y); 

  } 

  return false; 

} 

Questo è un pattern ben conosciuto e molto utilizzato, ma è verboso, ripetitivo (la parola Punto ripetuto 3 volte in 2 righe), soggetto ad errori ed il cast è scontato. 

Con il pattern matching per instanceof possiamo riscrivere il metodo equals in maniera meno verbosa e più robusta:  

public boolean equals(Object obj) { 

  if (obj instanceof Punto other) {  

    return (this.x == other.x &&  

      this.y == other.y); 

  } 

  return false; 

} 

Possiamo notare la variabile di binding other è stata dichiarata subito dopo il tipo (secondo operando dell’operatore instanceof). In questo modo abbiamo potuto evitare di definirla esplicitamente e di assegnare ad essa il cast del parametro obj. Notare anche che la variabile di binding other ha uno scope limitato al blocco di codice del costrutto if 

Potete approfondire l’argomento con l’articolo dedicato. 

Pattern matching per switch (Preview 17) 

Java 17 ritocca nuovamente il costrutto switch che era già stato modificato dalla versione 14.  

Con il pattern matching per switch, ora è possibile passare in input ad un costrutto switch un qualsiasi oggetto. In questo caso, le clausole case non coincideranno con delle costanti, bensì con dei tipi che sono compatibili gerarchicamente con il tipo specificato. Il controllo per eseguire il codice relativo ad una certa clausola case, quindi non sarà fatto verificando l’uguaglianza del parametro in input allo switch con una costante come è di solito avviene nel costrutto switch. Invece viene sfruttato l’operatore instanceof, che grazie al pattern matching definirà anche le variabili di binding per ogni case 

Per esempio, considerando le classi DiscoOttico, CD e DVD che abbiamo definito nel paragrafo delle sealed classes. Possiamo usare il pattern matching con il costrutto switch con la seguente sintassi: 

public class LettoreOttico { 

    public void inserisci(DiscoOttico discoOttico) { 

        switch(discoOttico) { 

            case CD cd -> suonaDisco(cd); 

            case DVD dvd -> caricaMenu(dvd); 

       } 

    } 

    // resto del codice omesso 

} 

In base al tipo a cui punterà al runtime il parametro discoOttico, il flusso di esecuzione eseguirà il codice specificato da uno dei due case. Notare come vengano dichiarate le variabili di binding subito dopo il tipo, e come possano essere utilizzate solo all’interno del relativo codice. 

Notare infine che non è necessario utilizzare la clausola default in casi come questi. Infatti l’utilizzo della classe astratta sealed DiscoOttico ci garantisce che come input questo switch può accettare solamente oggetti di tipo CD e DVD, e che quindi non è necessario aggiungere una clausola default perché tutti casi sono già stati coperti. Anche in questo caso la verbosità della sintassi del costrutto è stata notevolmente ridotta, mentre è aumentata la robustezza. 

In realtà questa caratteristica è ancora in anteprima in Java 17 e in Java 18, e probabilmente verrà ufficializzata solo nella versione 19. Questo significa che per compilare un’applicazione che fa uso del pattern matching per switch, bisogna specificare determinati flag come descritto nell’articolo dedicato alle caratteristiche in anteprima 

Anche in questo caso potete approfondire l’argomento con l’articolo dedicato. 

 

Conclusioni 

In questo articolo abbiamo visto come Java 17 abbia introdotto diverse novità rispetto a Java 11, che stanno rivoluzionando il linguaggio. La direzione dello sviluppo che ha impostato Oracle, ha come obiettivi principali quelli di rendere Java meno sempre più robusto, complesso, potente e meno verboso. E non è finita qui perché la pianificazione è molto lunga e guidata dalle richieste della community. Nelle prossimi mesi potremo già iniziare a provare in anteprima altre interessantissime novità come i virtual thread e la decostruzione degli oggetti 

Insomma, il meglio deve ancora venire! 

Claudio De Sio Cesari