Java Records in Profondità
Approfondimento sui record Java: costruttori canonici e compatti, validazione e casi d'uso pratici.
Un record è il modo di Java per dichiarare una classe il cui unico scopo è trasportare dati. Introdotto come anteprima in Java 14 e finalizzato in Java 16, un record elimina il solito codice ripetitivo — campi private final, un costruttore, metodi di accesso, equals, hashCode e toString — in un'unica riga di intestazione. Il capitolo precedente sui record ha mostrato la sintassi di base; questo approfondisce il comportamento effettivo dei record: i costruttori canonici e compatti, come impongono gli invarianti, quali garanzie di immutabilità si ottengono e dove si adattano (e dove no).
Cosa genera il compilatore per te
Quando scrivi record Point(int x, int y) {}, il compilatore produce una classe final con due campi private final, un costruttore pubblico che accetta entrambi, metodi di accesso pubblici con lo stesso nome dei componenti (x(), y() — senza prefisso get), e implementazioni di equals, hashCode e toString basate sui valori.
record Point(int x, int y) {}
// Equivalent to (roughly) hand-writing:
// final class Point {
// private final int x;
// private final int y;
// Point(int x, int y) { this.x = x; this.y = y; }
// int x() { return x; }
// int y() { return y; }
// public boolean equals(Object o) { ... compares x and y ... }
// public int hashCode() { ... derived from x and y ... }
// public String toString() { return "Point[x=" + x + ", y=" + y + "]"; }
// }x e y nell'intestazione sono i componenti del record. I membri generati dal compilatore derivano interamente da essi, nell'ordine di dichiarazione.
Costruttori canonici e compatti
Ogni record ha un costruttore canonico i cui parametri corrispondono ai componenti. Raramente lo si scrive per intero — si usa invece il costruttore compatto, che omette la lista dei parametri e le assegnazioni finali this.campo = campo. Il compilatore esegue prima il tuo codice, poi assegna i parametri (eventualmente modificati) ai campi. È il luogo naturale per la validazione e la normalizzazione.
record Range(int low, int high) {
Range { // compact constructor — no (int low, int high)
if (low > high) {
throw new IllegalArgumentException("low must be <= high");
}
low = Math.max(low, 0); // reassigning the parameter normalizes the field
}
}Se hai mai bisogno della forma canonica esplicita (ad esempio, per copiare in modo difensivo un componente mutabile), scrivi la firma completa ed esegui tu stesso le assegnazioni:
record Tags(String name, List<String> values) {
Tags(String name, List<String> values) { // explicit canonical constructor
this.name = name;
this.values = List.copyOf(values); // defensive, unmodifiable copy
}
}Immutabilità e cosa i record non sono
I campi di un record sono final, quindi il riferimento che ogni componente contiene non cambia mai dopo la costruzione. Questo rende i record superficialmente immutabili. Ma l'immutabilità si ferma al riferimento: se un componente punta a un oggetto mutabile (come un ArrayList), i chiamanti che condividono quell'oggetto possono comunque mutarne il contenuto. Le copie difensive nel costruttore canonico colmano questa lacuna.
| Proprietà | Record | Classi normali |
|---|---|---|
| Campi | sempre private final | a tua scelta |
| Classe | implicitamente final | estendibile a meno che non sia final |
| Superclasse | sempre java.lang.Record | qualsiasi (default Object) |
| Metodi di accesso | generati automaticamente, senza prefisso get | scritti a mano |
equals/hashCode | basati sui valori, generati | basati sull'identità per default |
| Setter | nessuno — immutabili | consentiti |
Poiché un record estende sempre java.lang.Record, non può estendere un'altra classe. Può comunque implementare interfacce, dichiarare membri statici e aggiungere metodi di istanza.
Aggiungere comportamenti, membri statici e factory
Un record è comunque una classe. Puoi fornirgli metodi extra, metodi factory statici, campi statici e persino tipi annidati. I componenti definiscono lo stato; tutto il resto è Java ordinario.
record Money(String currency, long cents) {
static Money of(String currency, long cents) { // static factory
return new Money(currency, cents);
}
Money plus(Money other) { // derived behavior
if (!currency.equals(other.currency)) {
throw new IllegalArgumentException("currency mismatch");
}
return new Money(currency, cents + other.cents); // returns a new value
}
}I record si abbinano naturalmente anche ai tipi sealed e al pattern matching, modellando insiemi chiusi di forme dati — la base della progettazione dati in stile algebrico nel Java moderno. Un'interfaccia sealed fissa l'insieme delle implementazioni record consentite, e uno switch su quei record può decostruire ciascuno tramite i suoi componenti in un'unica espressione.
Un esempio pratico: record dall'inizio alla fine
Questo programma esercita i membri generati di un record, verifica le proprietà di immutabilità e di classe tramite reflection, impone un invariante in un costruttore compatto, elenca i componenti del record nell'ordine di dichiarazione e mostra i record in uso con collezioni e comportamenti aggiuntivi.
Cosa trarre dall'esecuzione:
- Il
Pointper cui non hai mai scritto un corpo ha comunque stampatoPoint[x=3, y=4], risposto ada.x(), e riportatoequals by value: truecon hash code corrispondenti — il compilatore ha generatotoString, metodi di accesso,equalsehashCodebasati sui valori dai soli due componenti. - La reflection ha confermato il contratto garantito dal linguaggio:
is final class : true(i record non possono essere sottoclassati) eis a record : true(ogni record estendejava.lang.Record), motivo per cui non ci sono setter e i campi sono immutabili. - La chiamata
Range(9, 2)è stata rifiutata conlow must be <= high. Il costruttore compatto è eseguito prima dell'assegnazione dei campi, quindi un record non viene mai costruito in uno stato non valido — la validazione appartiene lì, non in un controllo factory separato. getRecordComponents()ha restituito i componenti nell'ordine di dichiarazione comelow:int high:int, mostrando che la struttura di un record è ispezionabile tramite reflection — la base per le librerie di serializzazione e i framework che mappano i record automaticamente.Money.of("USD", 500).plus(Money.of("USD", 250))ha prodottoUSD 750, edistinct()ha ridotto due valori identiciPoint(0,0)lasciando2— i record si comportano come valori propri ovunque, compresi stream e set, proprio perché il loroequals/hashCodeconfronta il contenuto.
Quando usare un record (e quando no)
Scegli un record quando il tipo è definito dai suoi dati e quei dati non cambiano dopo la costruzione:
- DTO e payload di richiesta/risposta API.
- Chiavi di mappe ed elementi di set (il confronto per valore di
equals/hashCodeè incluso gratuitamente). - Tipi di ritorno che raggruppano più valori, sostituendo tuple usa-e-getta o parametri di output.
- Le "foglie" di una gerarchia sealed che destrutturi con il pattern matching.
Preferisci una classe normale quando:
- L'oggetto ha stato mutabile o un ciclo di vita (entità, builder, servizi).
- Devi estendere un'altra classe — i record possono solo implementare interfacce.
- L'identità dell'oggetto conta più del suo contenuto (vuoi l'uguaglianza per riferimento).
Un errore comune: il metodo di accesso di un record restituisce il riferimento memorizzato così com'è. Se un componente è di tipo mutabile (una List, un array, una Date), copialo in modo difensivo nel costruttore canonico — come fa l'esempio Tags sopra con List.copyOf — altrimenti i chiamanti possono mutare lo stato "immutabile" del record tramite il riferimento passato.