Java – Breve introduzione ai Record di Java 14

Nel mese di Marzo di quest’anno è stato ufficialmente rilasciato il JDK 14 di Oracle, ovvero: Java 14. In questa versione sono state introdotte alcune novità tra cui una nuova tipologia di classe chiamata Record. In questo articolo fornirò una breve introduzione su questa nuova funzionalità.

Innanzitutto è bene precisare che in Java 14 i Record sono una preview feature, cioè una funzionalità completa e pienamente funzionante ma non ancora confermata in maniera definitiva (vedere il precedente articolo Breve spiegazione delle preview feature del JDK Oracle). Nella prossima release (Java 15) i Record quindi potrebbero essere confermati così come sono attualmente o in parte modificati oppure al contrario rimossi (eventualità poco probabile ma teoricamente possibile). Nonostante questo, è comunque sicuramente interessante dare una prima occhiata a questa novità.

I Record sono stati introdotti per dare la possibilità di definire una data class “immutabile”, cioè un tipo particolare di classe che ha lo scopo principale di contenere solamente dei dati. L’obiettivo dei Record non è solo quello di esprimere più chiaramente l’intenzione di voler creare una classe di “dati” ma anche di permettere questa definizione in maniera estremamente semplice e concisa.

Giusto per chiarire, in Java è sempre stato possibile definire una classe di dati (“mutabile” o “immutabile” che sia) ma con l’approccio tradizionale si devono scrivere molte cose “a mano”. Infatti per fare una classe di dati che sia immutabile è necessario come minimo:

  • scrivere esplicitamente tutti i campi (tipicamente dichiarati private e final)
  • scrivere almeno un costruttore per inizializzare i campi (poiché sono final)
  • scrivere tutti i metodi “accessori” (getter) necessari per l’accesso ai dati
  • scrivere possibilmente e preferibilmente anche i metodi standard degli oggetti cioè equals(), hashCode() e toString()

La realizzazione di tutti questi elementi non è una cosa particolarmente lunga o difficile. Tra l’altro gli IDE come Eclipse, NetBeans e IntelliJ IDEA hanno già delle opzioni per generare velocemente questi costruttori e metodi. Inoltre esistono anche delle librerie esterne come Lombok, AutoValue e Immutables che consentono di creare più facilmente queste tipologie di classi di dati utilizzando vari approcci e tecniche differenti.

Esempio di Record

Un Record si definisce in un sorgente utilizzando la parola record invece che la tradizionale parola class. A partire da Java 14 record è un restricted identifier, cioè una parola che ha un significato particolare solo in determinati contesti (questo della dichiarazione del Record) ma può essere usata come “identificatore” in altri contesti.

Supponiamo di voler definire un Record per rappresentare due valori minimo/massimo di tipo int. Questo di seguito è ciò che si può scrivere:

File sorgente: IntMinMax.java

public record IntMinMax(int min, int max) { }

Il codice è composto proprio esattamente da una sola riga! La parte che è racchiusa tra le parentesi tonde, ovvero (int min, int max) prende il nome di state description e serve ad elencare tutti i “componenti” del Record specificando per ciascuno il tipo e il nome. La forma di questa dichiarazione è in pratica strutturalmente uguale a quella di un costruttore.

Per compilare questo sorgente utilizzando il javac del JDK 14 è necessario specificare l’opzione per abilitare le preview feature e anche specificare la release (o il source level) da utilizzare. Quindi è necessario eseguire:

javac --enable-preview --release 14 IntMinMax.java

oppure

javac --enable-preview -source 14 IntMinMax.java

Dietro quella singola riga scritta nel sorgente c’è in realtà più codice che viene generato dal compilatore. Per verificare questo aspetto è sufficiente utilizzare il tool javap del JDK che permette di “disassemblare” il contenuto dei file .class.

Quindi è sufficiente eseguire:

javap -p IntMinMax

(nota: l’opzione -p serve per mostrare anche i membri private)

L’output che si ottiene è il seguente:

Compiled from "IntMinMax.java"
public final class IntMinMax extends java.lang.Record {
  private final int min;
  private final int max;
  public IntMinMax(int, int);
  public java.lang.String toString();
  public final int hashCode();
  public final boolean equals(java.lang.Object);
  public int min();
  public int max();
}

Ci sono svariate cose che si possono osservare sui Record, prendendo anche come riferimento l’output che è stato riportato.

  1. Nei tipi Record la ereditarietà è estremamente rigida e prefissata, in maniera molto similare a quanto già succede con le enum introdotte in Java 5. La classe è final, quindi non può essere a sua volta estesa. È lecito, se si vuole, mettere esplicitamente la parola final ma se viene omessa, la classe è comunque implicitamente final. Inoltre estende già la classe standard java.lang.Record e nella dichiarazione del record non si può usare esplicitamente la parola chiave extends. È però possibile implementare delle interfacce usando la classica clausola implements X, Y, ecc… .

  2. Viene generato automaticamente un campo marcato private final per ciascun componente dichiarato nella state description utilizzando esattamente il nome dichiarato nel sorgente, senza alcuna modifica.

  3. Viene generato automaticamente un unico costruttore che riceve tutti i dati esattamente nell’ordine in cui sono dichiarati nella state description. Il costruttore generato semplicemente e banalmente assegna ciascun parametro al corrispondente campo, senza fare alcun tipo di logica o controllo.

  4. Vengono generati i tre metodi standard degli oggetti toString, equals e hashCode. La implementazione di questi metodi è corretta ed appropriata e prende in considerazione tutti i componenti dichiarati nella state description. La implementazione del toString genera una stringa in una forma abbastanza semplice del tipo "IntMinMax[min=123, max=456]" .

  5. Vengono generati automaticamente dei metodi “accessori” per leggere il valore dei campi. Attenzione: questi metodi non seguono le ben note convenzioni dettate dalle specifiche JavaBeans, quindi non si chiamano es. getMin() e getMax(). Invece i nomi di questi metodi sono esattamente uguali ai nomi indicati nella state description, cioè min() e max() .

La denominazione dei metodi “accessori” che non segue le convenzioni dei JavaBeans è sicuramente la caratteristica più particolare, inaspettata e forse controversa dei nuovi Record ma c’è una motivazione ben precisa per questo. La JEP-359 (JEP = JDK Enhancement Proposal) che è stata scritta per proporre la introduzione dei Record, infatti riporta ad un certo punto la seguente affermazione:

It is not a goal to declare "war on boilerplate"; in particular, it is not a goal to address the problems of mutable classes using the JavaBean naming conventions. It is not a goal to add features such as properties, metaprogramming, and annotation-driven code generation, even though they are frequently proposed as "solutions" to this problem.

Ovvero l’obiettivo dei Record non è quello di indirizzare la realizzazione di queste data class sfruttando le convenzioni JavaBeans, né il concetto delle “proprietà”, né l’uso di annotation o altro.

In definitiva, per concludere questo articolo introduttivo, i Record sono semplicemente da vedere come un nuovo modo semplice e pratico per realizzare delle data class da utilizzare tipicamente per il passaggio di dati tra classi/metodi.