CodeGym /Corsi /JAVA 25 SELF /I contratti di equals e hashCode

I contratti di equals e hashCode

JAVA 25 SELF
Livello 29 , Lezione 0
Disponibile

1. Introduzione

Come confrontiamo gli oggetti

In Java gli oggetti non sono solo dati: ognuno ha un proprio indirizzo in memoria. L’operatore == risponde alla domanda «è la stessa scatola?», cioè confronta i riferimenti (gli indirizzi), non il contenuto. Il metodo equals è pensato per confrontare il contenuto. Per impostazione predefinita, se non viene ridefinito, equals si comporta come ==.

Person p1 = new Person("Ivan", 20);
Person p2 = new Person("Ivan", 20);

System.out.println(p1 == p2); // false — sono oggetti diversi in memoria!

Perché due oggetti diversi siano considerati uguali per dati (ad esempio, tutti i campi significativi coincidono), è necessario ridefinire equals. E se prevedete di usare gli oggetti in collezioni basate su hash, assicuratevi di ridefinire correttamente anche hashCode.

class Person {
    String name;
    int age;

    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true; // è lo stesso oggetto
        if (o == null || getClass() != o.getClass()) return false; // controlliamo la classe
        Person person = (Person) o; // facciamo il cast al tipo necessario
        return age == person.age && name.equals(person.name); // confrontiamo i campi
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age); // per funzionare con HashSet/HashMap
    }
}

Una tale ridefinizione è fondamentale per lavorare correttamente con HashSet/HashMap: senza equals e hashCode le collezioni considereranno diversi anche oggetti uguali nei dati.

La relazione di equivalenza in equals dipende dai vostri requisiti: si può confrontare su tutti i campi, su una parte dei campi (ad esempio, solo l’email di User) — è importante la coerenza e il rispetto del contratto.

Dove è particolarmente importante?

  • Nelle collezioni basate su tabelle hash: HashSet, HashMap, LinkedHashSet e altre.
  • Durante la ricerca e la rimozione di elementi nelle collezioni: senza un equals corretto, l’oggetto desiderato può «non trovarsi».
  • Nella logica di business: ad esempio, due User con la stessa email devono essere considerati lo stesso utente.

hashCode — a cosa serve?

Le collezioni hash (ad esempio, HashSet, HashMap) usano tabelle hash. Il metodo hashCode calcola un intero — «l’indirizzo del bucket» in cui finirà l’oggetto. Se due oggetti sono uguali secondo equals, i loro hashCode devono coincidere. Se violate questa regola, le collezioni inizieranno a comportarsi in modo imprevedibile.

2. Contratto di equals e hashCode

Contratto di equals

Requisiti principali per il comportamento di equals:

  • Riflessività: a.equals(a) è sempre true.
  • Simmetria: se a.equals(b) è true, allora anche b.equals(a) è true.
  • Transitività: se a.equals(b) e b.equals(c), allora anche a.equals(c) è true.
  • Coerenza: se gli oggetti non cambiano, il risultato delle chiamate rimane stabile.
  • Confronto con null: qualsiasi oggetto non è uguale a null.

Contratto di hashCode

  • Se due oggetti sono uguali secondo equals, i loro hashCode sono uguali.
  • Se gli oggetti non sono uguali, i loro hash code possono coincidere (le collisioni sono ammesse, ma indesiderabili).
  • Finché l’oggetto non cambia logicamente, il suo hashCode deve rimanere costante.

In altre parole, un hashCode uguale è una condizione necessaria ma non sufficiente di uguaglianza: lo stesso hash non garantisce l’uguaglianza secondo equals.

3. Implementazione di equals e hashCode: esempio

Consideriamo la classe Person, dove l’uguaglianza è determinata dai campi name e age.

public class Person {
    private String name;
    private int age;

    // Costruttore, getter, setter...

    @Override
    public boolean equals(Object o) {
        if (this == o) return true; // Confronto dei riferimenti
        if (o == null || getClass() != o.getClass()) return false; // Verifica della classe

        Person person = (Person) o; // Cast del tipo

        // Confrontiamo i campi
        return age == person.age &&
               (name != null ? name.equals(person.name) : person.name == null);
    }

    @Override
    public int hashCode() {
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + age; // 31 è una scelta comune come numero primo
        return result;
    }
}
  • Per prima cosa controlli veloci: riferimento e classe.
  • Poi confronto dei campi significativi.
  • In hashCode si usa il numero primo 31 per ridurre le collisioni.

Uso di Objects.equals e Objects.hash

A partire da Java 7, la classe Objects semplifica il codice e lo rende più sicuro rispetto a null:

import java.util.Objects;

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Person person = (Person) o;
    return age == person.age &&
           Objects.equals(name, person.name);
}

@Override
public int hashCode() {
    return Objects.hash(name, age);
}

4. equals, hashCode e compareTo: come sono collegati

Come sono collegati equals e compareTo?

L’interfaccia Comparable definisce il metodo compareTo, che restituisce un numero negativo/zero/positivo per «minore/uguale/maggiore». È auspicabile che da a.compareTo(b) == 0 segua a.equals(b). Il contrario non è obbligatorio.

Se si viola la coerenza (ad esempio, compareTo confronta solo per età, mentre equals per nome ed età), allora collezioni ordinabili come TreeSet/TreeMap possono comportarsi in modo inatteso: gli oggetti risultano «uguali» dal punto di vista dell’ordinamento, ma non uguali per contenuto.

equals e hashCode nelle collezioni

  • In HashSet e HashMap le operazioni di aggiunta/ricerca/rimozione si basano su un’implementazione corretta di equals e hashCode.
  • Senza ridefinizione, queste collezioni considerano gli oggetti «diversi», anche se i loro dati sono identici.

5. Esempi: come funziona nelle collezioni

HashSet: memorizzazione di oggetti unici

Set<Person> people = new HashSet<>();
people.add(new Person("Ivan", 20));
people.add(new Person("Ivan", 20)); // Duplicato

System.out.println(people.size()); // 1, se equals/hashCode sono implementati correttamente

Senza un equals/hashCode corretto finiranno nel set entrambi gli oggetti.

HashMap: ricerca per chiave

Map<Person, String> map = new HashMap<>();
Person p1 = new Person("Anna", 25);
Person p2 = new Person("Anna", 25);

map.put(p1, "Utente 1");
System.out.println(map.get(p2)); // "Utente 1", se equals/hashCode sono implementati correttamente

Senza il contratto la collezione restituirà null — per essa sono «chiavi diverse».

6. Best practices: consigli di implementazione

  • Includete in equals/hashCode tutti i campi che determinano l’«identità» dell’oggetto.
  • Non utilizzate campi mutabili (che cambiano dopo l’aggiunta alle collezioni) nel calcolo di hashCode.
  • Lasciate generare i metodi all’IDE — meno probabilità di refusi.
  • In equals verificate prima this == o, poi la classe, poi i campi.
  • Per confrontare campi oggetto usate Objects.equals.
  • Per l’hash code usate Objects.hash o un pattern col moltiplicatore 31.

7. Sfumature utili

Perché non si può usare solo hashCode?

Le collisioni sono inevitabili: oggetti diversi possono avere lo stesso hashCode. L’hash è solo un’indicazione rapida per il bucket; la decisione finale sull’uguaglianza la prende equals.

Si può non ridefinire equals e hashCode?

Solo se siete sicuri che gli oggetti non verranno mai confrontati per contenuto e non diventeranno chiavi/elementi unici nelle collezioni. In pratica è raro.

Differenza tra ==, equals e compareTo

Operatore/metodo Che cosa confronta? A cosa serve?
==
Riferimenti (indirizzi in memoria) Verifica: «è lo stesso oggetto?»
equals
Contenuto degli oggetti Uguaglianza secondo la logica di business
compareTo
Ordine (minore/uguale/maggiore) Ordinamento

8. Errori tipici nell’implementazione di equals e hashCode

Errore n. 1: avete ridefinito equals ma avete dimenticato hashCode. Gli oggetti risultano uguali ma finiscono in bucket diversi della tabella hash — ricerca e rimozione si compromettono.

Errore n. 2: usate campi mutabili in hashCode. Se un campo cambia dopo l’aggiunta alla collezione, l’oggetto si «perde»: l’hash cambia, ma il bucket no.

Errore n. 3: simmetria/transitività di equals violata. a.equals(b) restituisce true, mentre b.equals(a) è false, oppure si rompe la transitività — le collezioni iniziano a comportarsi in modo imprevedibile.

Errore n. 4: non controllate la classe in equals. Confrontare oggetti di classi diverse porta a risultati errati o eccezioni.

Errore n. 5: confrontate stringhe e oggetti con ==. L’operatore == confronta i riferimenti; usate equals per il contenuto.

Errore n. 6: manca coerenza tra compareTo ed equals. Se a.compareTo(b) == 0, ma !a.equals(b), le collezioni TreeSet/TreeMap possono considerare gli elementi uguali per l’ordine ma diversi per l’uguaglianza — fonte di «fantasmi» e duplicati.

Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION