1. Generazione automatica di equals, hashCode, toString
A cosa servono questi metodi?
Lavorando con gli oggetti in Java, ti imbatti abbastanza presto sempre negli stessi compiti. A volte devi verificare se due oggetti sono uguali. Per esempio, capire se è già presente in una collezione come Set o Map. In altri casi l’oggetto viene usato come chiave in una HashMap, e lì servono regole di confronto speciali. E quasi sempre vuoi stampare un oggetto nel log o a schermo in modo che l’output non sia solo un’accozzaglia come MyClass@7b23ec81, ma qualcosa di significativo.
Per questi casi ogni classe in Java ha tre metodi speciali:
- equals(Object o) si occupa di verificare l’uguaglianza.
- hashCode() assegna all’oggetto un «impronta» numerica, necessaria a collezioni tipo le tabelle hash.
- toString() restituisce una rappresentazione testuale utile dell’oggetto, che semplifica molto debug e stampa.
Perché è doloroso nelle classi normali?
Nelle classi normali questi metodi vanno scritti a mano. Ed è qui che iniziano noia e mal di testa. Ne esce un sacco di codice ripetitivo che ingombra solo la classe. È facilissimo sbagliare da qualche parte: dimenticare di confrontare un campo, calcolare male hashCode e poi inseguire bug misteriosi. E se aggiungi un nuovo campo alla classe, dovrai tornare in tutti questi metodi e riscrivere tutto.
Esempio di classe normale
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int x() { return x; }
public int y() { return y; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Point point = (Point) o;
return x == point.x && y == point.y;
}
@Override
public int hashCode() {
return 31 * x + y;
}
@Override
public String toString() {
return "Point[x=" + x + ", y=" + y + "]";
}
}
Sembra familiare? Sì, e questo è solo per due campi! E se fossero venti?
Come lo fa un record
Una classe record fa tutto questo per te. Basta dichiarare:
public record Point(int x, int y) { }
E Java genererà DA SOLA:
- Il costruttore
- I getter (x(), y())
- equals, hashCode, toString
Metodi generati automaticamente
- equals confronta tutti i componenti del record per valore.
- hashCode è calcolato su tutti i componenti.
- toString restituisce una stringa del tipo Point[x=1, y=2].
Vediamolo in azione!
public record Point(int x, int y) {}
public class Demo {
public static void main(String[] args) {
Point p1 = new Point(1, 2);
Point p2 = new Point(1, 2);
System.out.println(p1.equals(p2)); // true
System.out.println(p1.hashCode() == p2.hashCode()); // true
System.out.println(p1); // Point[x=1, y=2]
}
}
Output:
true
true
Point[x=1, y=2]
Tutto funziona come previsto, senza una sola riga di codice in più!
2. Perché è importante: collezioni, debug e sicurezza
Funzionamento corretto nelle collezioni
Immagina di usare oggetti come chiavi in una HashMap o come elementi in un HashSet. Se equals e hashCode sono implementati male, le collezioni si comporteranno in modo strano: non troveranno un elemento appena aggiunto oppure, al contrario, considereranno uguali due oggetti diversi.
Con le classi record puoi essere tranquillo: confronto e hash tengono sempre conto di tutti i componenti del record (nell’ordine in cui sono dichiarati).
Esempio: usare un record come chiave
import java.util.HashMap;
import java.util.Map;
public class Demo {
public static void main(String[] args) {
record Point(int x, int y) {}
Map<Point, String> map = new HashMap<>();
Point p1 = new Point(3, 4);
map.put(p1, "Hello!");
Point p2 = new Point(3, 4);
System.out.println(map.get(p2)); // "Hello!" — funziona!
}
}
Nota: p1 e p2 sono oggetti diversi (riferimenti diversi), ma contengono gli stessi valori dei campi, quindi sono considerati uguali. E maggiori dettagli su Map e HashMap li troverai al livello 26 :P
Comodità per il debug e il logging
Invece del triste Point@1a2b3c4d (come avviene per impostazione predefinita nelle classi normali), un record viene stampato in modo chiaro e informativo:
Point[x=3, y=4]
Questo fa risparmiare un sacco di tempo durante debug e logging.
3. Come funzionano equals, hashCode, toString all'interno di un record
Metodo equals
Una classe record implementa equals in modo che due oggetti siano considerati uguali se:
- Sono dello stesso tipo (stessa classe record)
- Tutti i loro componenti sono uguali (== per i primitivi, equals() per gli oggetti)
Esempio di confronto
Point p1 = new Point(1, 2);
Point p2 = new Point(1, 2);
Point p3 = new Point(1, 3);
System.out.println(p1.equals(p2)); // true
System.out.println(p1.equals(p3)); // false
Metodo hashCode
L’hash code è calcolato su tutti i componenti del record, di solito tramite il metodo standard Objects.hash(...).
System.out.println(p1.hashCode()); // Ad esempio, 994
System.out.println(p2.hashCode()); // Anche 994
System.out.println(p3.hashCode()); // Un numero diverso
Metodo toString
La rappresentazione testuale è sempre nel formato:
ClassName[field1=value1, field2=value2, ...]
System.out.println(p1); // Point[x=1, y=2]
4. Override di equals, hashCode, toString: quando e come?
A volte (raramente, ma succede) è necessario modificare il comportamento standard di questi metodi. Per esempio, vuoi che toString restituisca una stringa con un altro formato, oppure che il confronto avvenga solo su una parte dei campi.
Attenzione: se fai l’override di equals/hashCode, fallo con molta consapevolezza! Violare il loro «contratto» può portare a bug difficili da individuare.
Come eseguire l'override di un metodo
Basta dichiarare il tuo metodo dentro il corpo della classe record:
public record Point(int x, int y) {
@Override
public String toString() {
return "(" + x + "; " + y + ")";
}
}
Point p = new Point(3, 5);
System.out.println(p); // (3; 5)
Si possono sovrascrivere equals/hashCode?
Sì, ma è fortemente sconsigliato se non sei sicuro di quello che stai facendo. Per esempio, se vuoi che il confronto avvenga solo sul campo x (il che è già strano):
public record Point(int x, int y) {
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Point other)) return false;
return x == other.x;
}
@Override
public int hashCode() {
return Integer.hashCode(x);
}
}
Point p1 = new Point(1, 2);
Point p2 = new Point(1, 999);
System.out.println(p1.equals(p2)); // true (!)
Ma fai attenzione: se fai l’override di equals, devi sempre fare l’override anche di hashCode — altrimenti le collezioni funzioneranno in modo scorretto.
Best practice
- Se non sai esattamente perché fare l’override — non farlo!
- Per toString — puoi tranquillamente creare il tuo formato, se vuoi.
- Per equals/hashCode — solo se hai una ragione valida e capisci le conseguenze.
5. Pratica: confronto di oggetti e uso dei record nelle collezioni
Esempio: confronto di due record
public record User(String name, int age) {}
public class Demo {
public static void main(String[] args) {
User u1 = new User("Alice", 20);
User u2 = new User("Alice", 20);
User u3 = new User("Bob", 25);
System.out.println(u1.equals(u2)); // true
System.out.println(u1.equals(u3)); // false
System.out.println(u1.hashCode() == u2.hashCode()); // true
System.out.println(u1); // User[name=Alice, age=20]
}
}
Esempio: usare un record come chiave in HashMap
Immaginiamo di avere un’applicazione in cui memorizziamo il numero di visite degli utenti per nome ed età (non si sa mai, magari in un club ci sono due «Ivan 20 anni»).
import java.util.HashMap;
import java.util.Map;
public class Demo {
public static void main(String[] args) {
record User(String name, int age) {}
Map<User, Integer> visits = new HashMap<>();
User ivan20 = new User("Ivan", 20);
User ivan22 = new User("Ivan", 22);
visits.put(ivan20, 5);
visits.put(ivan22, 2);
// Verifichiamo che la ricerca per valore funzioni correttamente
System.out.println(visits.get(new User("Ivan", 20))); // 5
System.out.println(visits.get(new User("Ivan", 22))); // 2
}
}
Se equals e hashCode non fossero implementati correttamente, la ricerca non funzionerebbe. Maggiori dettagli su Map e HashMap li troverai nelle lezioni del livello 26 :P
6. Errori comuni con equals, hashCode, toString nelle classi record
Errore n. 1: aspettarsi di poter modificare i campi dopo la creazione.
I campi di un record sono sempre final e il confronto avviene sui loro valori fissati nel costruttore. Se in qualche modo «modifichi» lo stato interno (per esempio tramite un oggetto mutabile dentro un campo), confronto e hash possono diventare scorretti.
Errore n. 2: override di equals ma dimenticarsi di hashCode.
Se fai l’override di uno di questi metodi — fallo sempre anche per l’altro! Altrimenti le collezioni (HashSet, HashMap) si comporteranno in modo imprevedibile.
Errore n. 3: aspettarsi che toString abbia un altro formato.
Se ti serve un formato particolare della stringa, fai semplicemente l’override di toString. Per impostazione predefinita, il formato è sempre ClassName[field1=value1, field2=value2].
Errore n. 4: usare record per classi complesse con campi mutabili.
I campi di un record dovrebbero essere immutabili. Se come campo usi, ad esempio, un ArrayList e qualcuno ne modifica il contenuto, confronto e hash code possono «rompersi». Per i record è meglio usare solo tipi immutabili.
Errore n. 5: usare record per classi il cui comportamento non è quello di un value object.
Un record non è una «piccola classe con sintassi breve». È un value object, pensato per conservare un insieme di valori. Se hai logica complessa, stato mutabile o necessità di ereditarietà, usa una classe normale.
GO TO FULL VERSION