Al momento stai visualizzando Tipi record

Tipi record

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 16 abbia ufficialmente introdotto un nuovo tipo di Java che si va ad aggiungere alle classi, le interfacce, le enumerazioni e le annotazioni: i record. Si tratta di particolari classi che godono della possibilità di essere definite con una sintassi particolarmente sintetica. Sono progettati per implementare classi che rappresentano dati. In particolare i record sono progettati per rappresentare contenitori di dati immutabili. La sintassi dei record aiuta gli sviluppatori a concentrarsi sulla progettazione di tali dati, senza perdersi nei dettagli implementativi.

 

Sintassi

La sintassi di un record è minimale:

[modificatori] record identificatore (header) {[membri]}

Con il termine header intendiamo una lista di dichiarazioni di variabili separate da virgole, e che rappresenteranno le variabili d’istanza del record. Un record definisce automaticamente un costruttore che prende come parametri l’header, definisce i metodi accessor di tutti i campi dichiarati nell’header e fornisce un’implementazione di default dei metodi toString, equals e hashCode.

Vediamo subito un esempio. Supponiamo quindi di voler scrivere un’applicazione per la vendita all’asta di quadri. Questi ultimi sono da intendere come oggetti immutabili. Infatti, una volta che essi sono stati messi in vendita non possono essere modificati. Per esempio, un quadro non può cambiare il suo titolo dopo essere stato definito. Possiamo quindi creare il record Quadro:

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

Possiamo istanziare questo record come se fosse una classe con un costruttore definito con la lista dei parametri dell’header:

Quadro quadro = new Quadro(“Camaleón”, “Leonardo Furino”, 1000000);

Quadro quadro = new Quadro("Camaleón", "Leonardo Furino", 1000000);

Siccome un record definisce automaticamente anche il metodo toString, il seguente snippet:

System.out.println(quadro);

produrrà l’output:

Quadro[titolo=Camaleón, autore=Leonardo Furino, prezzo=1000000]

Uno dei vantaggi evidenti dei record, è quindi la sintassi estremamente sintetica.

 

Record, enumerazioni e classi

Ci sono delle chiare similitudini tra i tipi record e i tipi enumerazioni. Entrambi i tipi, sostituiscono le classi in particolari situazioni. Le enumerazioni sono progettate per rappresentare un numero definito di istanze costanti dello stesso tipo. I record invece dovrebbero rappresentare contenitori di dati immutabili. Come le enumerazioni, anche i record semplificano il lavoro dello sviluppatore, offrendo una sintassi meno verbosa rispetto alle classi, e regole semplici e chiare.

I record sono stati introdotti solo con Java 14 come feature preview ed ufficializzati con Java 16. Come sempre, Java mitiga l’impatto di questa nuova caratteristica delegando al compilatore il compito di trasformare i record in classi per mantenere la retrocompatibilità con i vecchi programmi. In particolare, come le enumerazioni vengono trasformate dal compilatore in classi che estendono la classe astratta java.lang.Enum, i record vengono trasformati dal compilatore in classi che estendono la classe astratta java.lang.Record.

Come nel caso della classe Enum, il compilatore non permetterà allo sviluppatore di creare classi che estendono direttamente la classe Record. Anche essa infatti, è una classe speciale creata appositamente per supportare il concetto di record.

Quando compileremo il file Quadro.java, otterremo il file Quadro.class. In questo file, il compilatore avrà inserito una classe Quadro (risultato della conversione del record) che:

  • è dichiarata final;
  • definisce un costruttore che prende come parametri l’header;
  • definisce i metodi accessor di tutti i campi dichiarati nell’header;
  • ridefinisce i metodi di Object: toString, equals e hashCode.

Infatti, lo strumento javap del JDK, ci permette di leggere tramite l’introspezione la struttura della classe generata Quadro.class, con il seguente comando:

javap Quadro.class
Compiled from "Quadro.java"
public final class Quadro extends java.lang.Record {
  public Quadro(java.lang.String, java.lang.String, int);
  public java.lang.String toString();
  public final int hashCode();
  public final boolean equals(java.lang.Object);
  public java.lang.String titolo();
  public java.lang.String autore();
  public int prezzo();
}

Si noti che gli identificatori dei metodo di accesso (accessor), non seguono la solita convenzione che abbiamo usato sinora. i metodi infatti, invece di chiamarsi getTitolo, getAutore e getPrezzo, si chiamano semplicemente titolo, autore e prezzo, tuttavia la funzionalità rimane invariata.

Possiamo quindi accedere in lettura ai singoli campi del record tramite la seguente sintassi:

String titolo = quadro.titolo();
String autore = quadro.autore();

 

Se non ci fossero i record

Se avessimo creato una classe Quadro equivalente al record, avremmo dovuto scrivere manualmente il codice 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 titolo() {
        return titolo;
    }

    public String autore() {
        return autore;
    }

    public int prezzo() {
        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 + "]" ;
    }
}

Sembra evidente che in questo caso definire un record anziché una classe, sia indubbiamente più conveniente, nonostante un IDE ci avrebbe comunque permesso uno sviluppo di questa classe semi-automatizzato.

 

Ereditarietà e Polimorfismo

I record sono stati concepiti per rappresentare oggetti che trasportano dati immutabili. Per tale ragione l’ereditarietà dei record non è implementabile. In particolare, un record non può essere esteso dal momento che i record sono automaticamente dichiarati final. Inoltre, un record non può estendere una classe (ed ovviamente non può estendere un record) visto che già estende la classe Record.

È una scelta che sembra limitante, me è coerente con la filosofia di utilizzo dei record. Un record deve essere immutabile e l’ereditarietà non è compatibile con l’immutabilità. Tuttavia, estendendo implicitamente la classe Record, un record eredita i metodi di tale classe. In realtà, la classe Record fa solo override dei 3 metodi ereditati dalla classe Object: toString, equals e hashCode, e non definisce nuovi metodi.

All’interno di un record possiamo anche fare override sia dei metodi accessor, sia dei tre metodi di Object che il compilatore genererebbe in fase di compilazione. Potrebbe infatti essere utile dichiararli esplicitamente all’interno del nostro codice per personalizzarli ed ottimizzarli nel caso ce ne fosse bisogno. Per esempio, potremmo personalizzare il metodo toString nel record Quadro nel seguente modo:

public record Quadro(String titolo, String autore, int prezzo) { 
    @Override 
    public String toString() {
        return "Il quadro " +  titolo + " di " + autore + " costa " + prezzo;
    }
}

Inoltre già sappiamo che i record, come le enumerazioni, non possono essere estesi e né possono estendere altre classi o record. I record però, possono implementare interfacce.

Come per le enumerazioni, anche i record sono implicitamente final e quindi non è possibile usare il modificatore abstract. Quindi, nel momento in cui implementiamo un’interfaccia in un record, dovremo implementare obbligatoriamente tutti i metodi ereditati.

 

Personalizzazione di un record

In un record non è possibile dichiarare variabili d’istanza ed inizializzatori d’istanza. Questo per non violare il ruolo del record che dovrebbe essere quello di rappresentare un contenitore di dati immutabili.

È possibile invece dichiarare metodi, variabili e inizializzatori statici. Questi infatti, essendo statici, sono condivisi da tutte le istanze del record e non hanno accesso ai membri d’istanza di un particolare oggetto.

Ma la parte più interessante della personalizzazione di un record, riguarda la possibilità di creare anche i costruttori.

Sappiamo che in una classe, se non inseriamo costruttori viene inserito dal compilatore un costruttore senza parametri chiamato costruttore di default. Quando inseriamo esplicitamente un costruttore all’interno di una classe, qualsiasi sia il suo numero di parametri, il compilatore non inserirà più il costruttore di default.

In un record invece, il costruttore che aggiunge automaticamente il compilatore, definisce come parametri le variabili definite nell’header del record. Questo costruttore viene detto costruttore canonico (in inglese canonical constructor). Tra le sue caratteristiche ha quella di essere l’unico costruttore a cui è permesso settare le variabili d’istanza di un record (come vedremo tra poco). Detto questo, le nostre possibilità di definire costruttori, sono le seguenti:

  • ridefinire esplicitamente il costruttore canonico, meglio se con la sua forma compatta;
  • definire un costruttore non canonico che invoca il costruttore canonico;

 

Costruttore canonico

Possiamo esplicitamente dichiarare un costruttore canonico. Questo ci può servire se per esempio vogliamo aggiungere dei controlli di consistenza, prima di settare il valore delle variabili d’istanza. Per esempio, consideriamo il seguente record che astrae il concetto di foto, a cui aggiungiamo un costruttore canonico esplicitamente:

public record Foto(String formato, boolean aColori) {
    public Foto(String formato, boolean aColori) {
        if (formato.length() < 5) throw new 
            IllegalArgumentException("Descrizione del formato troppo breve");
        this.formato = formato;
        this.aColori = aColori;
    }
}

Si noti che è obbligatorio inizializzare le variabili d’istanza, altrimenti il compilatore ci segnalerà un errore. Se non inizializzassimo per esempio la variabile formato, otterremo il seguente errore:

error: variable formato might not have been initialized
    }
    ^
1 error

In questo caso abbiamo quindi creato esplicitamente un costruttore canonico che deve definire la stessa lista di parametri definita nell’header del record. Tuttavia, possiamo semplificare la creazione di un costruttore canonico esplicito utilizzando la sua forma compatta.

 

Costruttore canonico compatto

È possibile infatti creare un costruttore canonico compatto. Esso è caratterizzato dal fatto che non dichiara la lista dei parametri. Questo non significa che avrà una lista di parametri vuota, ma che accanto all’identificatore del costruttore non saranno presenti neanche le parentesi tonde. Riscriviamo quindi un costruttore equivalente a quello dell’esempio precedente:

public Foto {

if (formato.length() < 5) throw new IllegalArgumentException(

“Descrizione del formato troppo breve”);

}

public Foto {
    if (formato.length() < 5) throw new IllegalArgumentException(
        "Descrizione del formato troppo breve");
}

L’utilizzo dei costruttori canonici compatti, dovrebbe essere considerato il modo standard per definire costruttori esplicitamente in un record. Notare che non è stato neanche necessario inizializzare le variabili d’istanza che vengono inizializzate automaticamente. Ad essere più precisi, se provassimo ad inizializzare le variabili d’istanza in un costruttore canonico compatto otterremo un errore in compilazione.

 

Costruttore non canonico

È possibile anche definire un costruttore con una lista di parametri differente da quella del costruttore canonico, ovvero un costruttore non canonico. In questo caso, stiamo effettuando un overload di costruttori. Infatti, a differenza del costruttore di default delle classi, l’inserimento di un costruttore con differente lista di parametri, non impedirà al compilatore di inserire ugualmente il costruttore canonico. Inoltre, un costruttore non canonico deve invocare come prima istruzione un altro costruttore. Infatti, se inseriamo il seguente costruttore:

public Foto(String formato, boolean aColori, boolean msg) {
    if (formato.length() < 5) throw new IllegalArgumentException(msg);
    this.formato = formato;
    this.aColori = aColori;
}

otterremo un errore in compilazione:

Error: constructor is not canonical, so its first statement must invoke another constructor
    public Foto(String formato, boolean aColori, String msg) {
           ^
1 error

Chiaramente se inserissimo altri costruttori non canonici da chiamare, arriverà prima o poi il momento di invocare il costruttore canonico, esplicito o implicito che sia. Nel nostro esempio, se chiamiamo quindi direttamente il costruttore canonico, dovremo eliminare anche le istruzioni per settare le variabili d’istanza, visto che queste saranno settate dal costruttore canonico, dopo che esso è stato invocato nella prima riga del costruttore non canonico. Infatti, il seguente costruttore:

public Foto(String formato, boolean aColori, String msg) {
    this(formato, aColori);
    if (formato.length() < 5) throw new IllegalArgumentException(msg);
    this.formato = formato;
    this.aColori = aColori;
}

causerà i seguenti errori in compilazione:

error: variable formato might already have been assigned
        this.formato = formato;
            ^
error: variable aColori might already have been assigned
        this.aColori = aColori;
            ^
2 errors

che ci segnalano che le due variabili a questo punto saranno già inizializzate. Questo dimostra che è il costruttore canonico, ad avere sempre la responsabilità di settare le variabili d’istanza di un record. Quindi non ci resta che eliminare le righe superflue:

public Foto(String formato, boolean aColori, String msg) {
    this(formato, aColori);
    if (formato.length() < 5) throw new IllegalArgumentException(msg);
}

A questo punto potremo creare oggetti dal record Foto, sia con il costruttore canonico che con quello non canonico. Per esempio:

var foto1 = new Foto("Foto 1" , true); // costruttore canonico
System.out.println(foto1);
var foto2 = new Foto("Foto 2" , false, "Errore!"); // costruttore non canonico
System.out.println(foto2);
var foto3 = new Foto("Foto" , true, "Errore!"); // costruttore non canonico
System.out.println(foto3);

il codice precedente stamperà l’output:

Foto[formato=Foto 1, aColori=true]
Foto[formato=Foto 2, aColori=false]
Exception in thread "main" java.lang.IllegalArgumentException: Errore!
    at Foto.<init>(Foto.java:8)
    at TestRecordConstructors.main(TestRecordConstructors.java:7)

 

Quando utilizzare un record

Dovrebbe già essere chiaro quando utilizzare un record anziché una classe. Come già asserito, i record sono progettati per rappresentare contenitori di dati immutabili. I record non possono essere usati sempre al posto delle classi, specialmente quando si parla di classi che definiscono soprattutto metodi di business.

La natura del software è comunque quella di evolversi. È quindi possibile che, anche se creiamo un record per rappresentare un contenitore di dati immutabili, non è detto che un giorno non sia opportuno trasformarlo in classe. Un indizio che dovrebbe portarci a preferire di riscrivere un record sotto forma di classe, è quando abbiamo aggiunto troppi metodi o esteso troppe interfacce. In tali casi, è opportuno chiedersi se il record abbia bisogno di essere trasformato in classe.

Un record si adatta bene con le interfacce sealed, per la sua natura di immutabilità. Inoltre, solitamente non rappresenta concetti che aggregano un numero alto di variabili d’istanza.

Il concetto di record sembra adattarsi molto bene invece all’implementazione del design pattern noto come DTO (acronimo di Data Transfer Object).

 

Conclusioni

I record rappresentano un importante passo in avanti per il linguaggio Java. Si tratta sicuramente di una delle novità che sarà più apprezzata nel tempo dai programmatori. Infatti essi non saranno più obbligati ad aggiungere tramite IDE i soliti metodi di accesso e le implementazioni dei metodi ereditati da Object. Azioni noiose e di solito eseguite con sufficienza, che possono anche portare all’introduzione di bachi. In particolare, i record ci permettono di concentrarci sulla progettazione dei dati senza dover entrare nei dettagli implementativi, che comunque abbiamo sempre la possibilità di personalizzare. Inoltre la natura immutabile dei record ci indirizzerà nello scrivere programmi più semplici ed efficienti.

 

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