CodeGym /Corsi /JAVA 25 SELF /equals, hashCode, toString: generazione automatica

equals, hashCode, toString: generazione automatica

JAVA 25 SELF
Livello 22, Lezione 2
Disponibile

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.

1
Compito
JAVA 25 SELF, livello 22, lezione 2
Bloccato
Dettagli del libro nel rapporto della biblioteca 📚
Dettagli del libro nel rapporto della biblioteca 📚
1
Compito
JAVA 25 SELF, livello 22, lezione 2
Bloccato
Identificazione della posizione sulla mappa 📍
Identificazione della posizione sulla mappa 📍
1
Compito
JAVA 25 SELF, livello 22, lezione 2
Bloccato
Rappresentazione elegante del profilo utente 🧑‍💻
Rappresentazione elegante del profilo utente 🧑‍💻
1
Compito
JAVA 25 SELF, livello 22, lezione 2
Bloccato
Identificazione dei prodotti per nome nel negozio 🏷️
Identificazione dei prodotti per nome nel negozio 🏷️
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION