1. Confronto tra record e class: quali sono le principali differenze?
In Java abbiamo due modi principali per definire i nostri tipi di dati: tramite le classi tradizionali (class) e tramite le classi record (record). A prima vista, entrambe le opzioni permettono di memorizzare ed elaborare dati. Ma, se andiamo un po’ più a fondo, le differenze sono più numerose di quanto sembri!
Tabella delle differenze: class vs record
| Caratteristica | Classe tradizionale (class) | Classe record (record) |
|---|---|---|
| Mutabilità | Qualsiasi: è possibile avere campi final o meno | Immutabile: tutti i campi sono final |
| Ereditarietà | È possibile estendere (extends), non è final di default | Sempre final, non può essere una superclasse |
| Campi | Qualsiasi: statici, non statici, final o non-final, di qualsiasi tipo | Solo componenti del record (private final), più campi statici |
| Getter/setter | Li scriviamo noi (o li generiamo con Lombok) | I getter vengono creati automaticamente (il nome del campo coincide con il nome del metodo), non ci sono setter |
| equals/hashCode/toString | Di solito si scrivono a mano/si generano (equals, hashCode, toString) | Generati automaticamente per tutti i componenti |
| Costruttori | Qualsiasi, quanti se ne vuole | Uno principale (per tutti i componenti), è possibile aggiungere un costruttore compatto |
| Interfacce | Possono essere implementate | Possono essere implementate |
| Metodi aggiuntivi | Qualsiasi | Si possono aggiungere, ma solo metodi (non campi) |
| Uso nelle collezioni | Possibile, ma serve implementare correttamente equals/hashCode | Ideale come chiavi/valori, tutto è già implementato |
Esempio illustrativo
Classe tradizionale:
public class Person {
private final String name;
private final int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
@Override
public boolean equals(Object o) { /* ... */ }
@Override
public int hashCode() { /* ... */ }
@Override
public String toString() { /* ... */ }
}
Classe record:
public record Person(String name, int age) { }
Fatto! Una sola riga di codice — e otteniamo lo stesso risultato (e persino meglio). E nessun rischio di dimenticare di implementare qualcosa di importante.
2. Limitazioni dei record
Le classi record non sono semplicemente «sintassi breve», ma una vera e propria concezione con regole rigorose. Vediamole più in dettaglio.
I record sono sempre final
Una classe record per definizione è sempre final. Questo significa che non puoi creare una sottoclasse di un record:
public record Point(int x, int y) { }
// public class ColoredPoint extends Point { } // Errore di compilazione!
Se hai bisogno di estendere il comportamento, usa classi tradizionali o la composizione (incapsula il record in una classe).
Un record non può essere una superclasse
Un record non può essere padre di altre classi, è sempre final. È logico: se fosse possibile, qualcuno potrebbe aggiungere un campo mutabile — e l’intera idea dei «dati immutabili» crollerebbe.
Solo campi final (componenti)
Tutti i componenti del record sono dichiarati nell’intestazione e sono per impostazione predefinita private final. Non puoi aggiungere campi non statici nel corpo del record:
public record User(String login, String email) {
// int counter; // Errore! Non è consentito aggiungere campi non statici
static int totalUsers = 0; // Consentito, è un campo statico
}
Nessun setter
Una classe record non può avere setter per i componenti. Qualsiasi tentativo di aggiungere un metodo come setX(int x) sarebbe privo di senso: non potrai modificare il valore del campo dopo la creazione dell’oggetto.
public record Point(int x, int y) {
// public void setX(int x) { this.x = x; } // Errore: impossibile modificare un campo final
}
Nessun costruttore vuoto
Una classe record ha sempre e solo il costruttore principale, che accetta i valori per tutti i componenti. Non è possibile creare un record senza specificare tutti i dati:
Point p = new Point(1, 2); // OK
// Point p = new Point(); // Errore: nessun costruttore senza parametri
Nessun inizializzatore non statico
Una classe record non può contenere inizializzatori non statici (quelli scritti tra parentesi graffe fuori dai metodi):
public record User(String login) {
// { /* ... */ } // Errore: gli inizializzatori non statici sono vietati
}
Limitazioni sull’ereditarietà
Una classe record non può estendere esplicitamente un’altra classe (tranne java.lang.Record, che è la classe base nascosta per tutti i record). Ma l’implementazione delle interfacce è consentita!
public interface Printable {
void print();
}
public record Book(String title) implements Printable {
@Override
public void print() {
System.out.println("Stampiamo il libro: " + title);
}
}
Non adatto a una logica di business complessa
Un record riguarda i dati, non il comportamento. Se il tuo oggetto ha una logica complessa, uno stato mutabile, un «ciclo di vita» o molte dipendenze, il record non aiuterà. Meglio usare una classe tradizionale.
3. Quando conviene usare i record?
- DTO (Data Transfer Object): per trasferire dati immutabili tra livelli dell’applicazione, servizi, microservizi o controller REST (ad esempio nelle risposte JSON).
- Value Object: oggetti che sono definiti unicamente dai loro valori.
- Chiavi e valori nelle collezioni: quando è importante una corretta implementazione di equals e hashCode (ad esempio per l’uso in HashMap o Set).
- Risultati di calcoli: quando occorre restituire dal metodo più valori contemporaneamente (ad esempio, record Pair<T, U>(T first, U second)).
Esempio: DTO per un controller REST
public record UserDto(String login, String email) { }
Ora si può restituire tranquillamente un oggetto di questo tipo dal controller, senza temere che qualcuno ne modifichi i campi.
Esempio: Chiave per HashMap
public record Point(int x, int y) { }
Map<Point, String> pointNames = new HashMap<>();
pointNames.put(new Point(1, 2), "A");
pointNames.put(new Point(3, 4), "B");
// Tutto funziona correttamente: equals e hashCode sono già implementati!
4. Quando NON conviene usare i record
- Stato mutabile: se almeno un campo deve poter cambiare dopo la creazione dell’oggetto.
- Logica complessa: se l’oggetto ha un comportamento complesso, molti metodi, oggetti annidati con stato mutabile.
- Ereditarietà: se serve una gerarchia di classi, classi base astratte, override dei metodi.
- Entità di business: ad esempio oggetti che vivono nel database e hanno un identificatore univoco.
Esempio: quando serve una classe tradizionale
public class Account {
private String id;
private int balance;
public Account(String id, int balance) {
this.id = id;
this.balance = balance;
}
public void deposit(int amount) { balance += amount; }
public void withdraw(int amount) { balance -= amount; }
// getters, setters, equals, hashCode, toString...
}
Qui è evidente che lo stato dell’oggetto cambia — un record non è adatto.
5. Esempi pratici: scegliere tra record e class
Esempio 1: record — la scelta ideale
public record Rectangle(int width, int height) {
public int area() {
return width * height;
}
}
- Il rettangolo è definito solo da larghezza e altezza.
- Non c’è necessità di modificare questi valori dopo la creazione.
- Si può aggiungere un metodo utile come area().
- Il resto lo farà Java per voi.
Esempio 2: class — l’opzione migliore
public class MutableRectangle {
private int width;
private int height;
public MutableRectangle(int width, int height) {
this.width = width;
this.height = height;
}
public void setWidth(int width) { this.width = width; }
public void setHeight(int height) { this.height = height; }
public int area() { return width * height; }
}
Serve modificare le dimensioni del rettangolo dopo la creazione? Usa una classe tradizionale.
6. Errori tipici nell’uso dei record
Errore n. 1: tentativo di aggiungere un campo non statico.
Una classe record non consente di dichiarare campi non statici al di fuori dell’elenco dei componenti. Se ci si prova, il compilatore segnalerà un errore. Ad esempio:
public record City(String name) {
// int population; // Errore!
}
Errore n. 2: voler aggiungere un setter.
Un record non supporta setter per i componenti. Qualsiasi tentativo di modificare il valore di un campo dopo la creazione dell’oggetto comporta un errore di compilazione.
Errore n. 3: tentativo di ereditare da un record o di far ereditare un record.
Un record è sempre final. Non si può ereditare da un record e un record non può estendere un’altra classe (tranne il java.lang.Record nascosto).
Errore n. 4: utilizzo dei record per oggetti mutabili.
Se prevedi di modificare lo stato dell’oggetto dopo la creazione, il record non fa per te! Usa una classe tradizionale.
Errore n. 5: dimenticare le limitazioni del costruttore.
Una classe record deve avere un costruttore che accetti i valori per tutti i componenti. Non esiste un costruttore senza parametri!
GO TO FULL VERSION