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?» |
|
Contenuto degli oggetti | Uguaglianza secondo la logica di business |
|
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.
GO TO FULL VERSION