W3docs

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àRecordClassi normali
Campisempre private finala tua scelta
Classeimplicitamente finalestendibile a meno che non sia final
Superclassesempre java.lang.Recordqualsiasi (default Object)
Metodi di accessogenerati automaticamente, senza prefisso getscritti a mano
equals/hashCodebasati sui valori, generatibasati sull'identità per default
Setternessuno — immutabiliconsentiti

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.

java— editable, runs on the server

Cosa trarre dall'esecuzione:

  • Il Point per cui non hai mai scritto un corpo ha comunque stampato Point[x=3, y=4], risposto ad a.x(), e riportato equals by value: true con hash code corrispondenti — il compilatore ha generato toString, metodi di accesso, equals e hashCode basati 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) e is a record : true (ogni record estende java.lang.Record), motivo per cui non ci sono setter e i campi sono immutabili.
  • La chiamata Range(9, 2) è stata rifiutata con low 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 come low: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 prodotto USD 750, e distinct() ha ridotto due valori identici Point(0,0) lasciando 2 — i record si comportano come valori propri ovunque, compresi stream e set, proprio perché il loro equals/hashCode confronta 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.

Esercizio

Pratica
Cosa permette di fare il costruttore compatto di un record (ad esempio 'Range { ... }') che un corpo di costruttore esplicito ordinario richiederebbe altrimenti più codice per fare?
Cosa permette di fare il costruttore compatto di un record (ad esempio 'Range { ... }') che un corpo di costruttore esplicito ordinario richiederebbe altrimenti più codice per fare?
Was this page helpful?