Java equals() e hashCode()
Impara a fare correttamente l'override di equals() e hashCode() nelle classi Java per supportare le collezioni e l'uguaglianza basata sui valori.
equals e hashCode sono i due metodi di Object su cui le collezioni basate su hash — HashMap, HashSet, LinkedHashMap, qualsiasi struttura che usa l'hashing — fanno affidamento in modo silenzioso. Implementali correttamente e i tuoi oggetti si comporteranno come valori: set.contains(point) troverà il punto indipendentemente dall'istanza new Point(3, 4) che passi. Implementali in modo errato e otterrai duplicati nei set, chiavi mancanti nelle mappe e bug che emergono solo sotto carico.
Il comportamento predefinito ereditato da Object confronta l'identità: due riferimenti sono uguali solo quando puntano allo stesso oggetto. Questo va bene per cose come le connessioni al database, dove ogni istanza è una risorsa distinta. Per classi che rappresentano valori — denaro, punti, nomi, date — quasi sempre si vuole invece l'uguaglianza basata sul contenuto, e questo significa fare l'override di entrambi i metodi insieme.
Il contratto
equals deve soddisfare quattro regole:
- Riflessivo —
x.equals(x)è true. - Simmetrico —
x.equals(y)se e solo sey.equals(x). - Transitivo — se
x.equals(y)ey.equals(z), allorax.equals(z). - Consistente — chiamate ripetute con campi invariati restituiscono la stessa risposta.
In più: x.equals(null) deve restituire false, senza mai lanciare eccezioni.
hashCode ha una regola che lo lega a equals:
- Oggetti uguali devono avere hash code uguali. Oggetti diversi possono condividere un hash code (le collisioni sono consentite, ma penalizzano le prestazioni).
Questa singola regola è il motivo per cui non si può fare l'override di uno senza fare l'override dell'altro. Se a.equals(b) ma a.hashCode() != b.hashCode(), HashSet li inserisce in bucket diversi, contains trova quello sbagliato e si ha un duplicato fantasma.
Osservare la violazione del contratto
Questa classe fa l'override di equals ma dimentica hashCode, quindi eredita l'hash basato sull'identità di Object. I due oggetti sono "uguali" eppure finiscono in bucket diversi — contains non riesce a trovare quello appena aggiunto:
equals dice che gli oggetti sono uguali, ma il set non riesce a trovare il secondo. Fai l'override di hashCode per farlo corrispondere e la ricerca avrà successo.
Anatomia di un equals corretto
Un equals funzionante segue una struttura standard:
public final class Point {
private final int x;
private final int y;
public Point(int x, int y) { this.x = x; this.y = y; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Point p)) return false;
return x == p.x && y == p.y;
}
@Override
public int hashCode() {
return Objects.hash(x, y);
}
}Passo dopo passo:
- Short-circuit sull'identità.
this == ogestisce velocemente il caso comune. - Controllo del tipo con binding.
instanceof Point prifiuta null e tipi errati in un'unica espressione e associa il riferimento restretto. - Confronto dei campi. Usa
==per i primitivi,Objects.equals(a, b)per i riferimenti nullable,Float.compare/Double.compareper i float.
Objects.hash(...) costruisce un hash da un elenco di campi. È leggermente più lento del codice XOR/moltiplicazione scritto a mano, ma è corretto e privo di ambiguità.
getClass o instanceof?
Due scuole di pensiero:
instanceofconsente a un'istanza di una sottoclasse di essere uguale a un'istanza della classe padre se l'insieme di campi confrontati è lo stesso. Leggermente più flessibile.getClass()esige la classe runtime esatta. Più facile da mantenere simmetrico nelle gerarchie, ma rompe la sostituibilità.
Per la maggior parte delle classi che rappresentano valori, il percorso più semplice è rendere la classe final e usare instanceof. Senza final, mescolare i due stili in una gerarchia è dove vive la maggior parte dei bug di uguaglianza. I record evitano del tutto questa decisione — sono implicitamente final e l'equals generato usa un controllo del tipo esatto.
Campi in virgola mobile
Non usare == su campi double o float — il valore +0.0 è uguale a -0.0 con ==, ma Double.compare li tratta diversamente, e NaN == NaN è false. Double.compare(a, b) == 0 e Float.compare forniscono la risposta consistente richiesta dal contratto.
Array
Object.equals su un array confronta i riferimenti, non il contenuto. Usa Arrays.equals(a, b) per array monodimensionali, Arrays.deepEquals per quelli multidimensionali. Analogamente, usa Arrays.hashCode / Arrays.deepHashCode in hashCode.
La mutabilità è ostile alle collezioni basate su hash
Se si modifica un campo che fa parte di equals/hashCode dopo aver inserito l'oggetto in un HashSet, il bucket in cui il set lo ha collocato non corrisponde più al nuovo hash — e l'oggetto diventa irraggiungibile tramite contains. La regola più sicura: i campi usati in equals dovrebbero essere final. Se non è possibile, non inserire mai l'oggetto in una collezione basata su hash.
Non scrivere nessuno dei due a mano per semplici classi dati
Se la classe è un puro contenitore di dati, preferisci un record — il compilatore genera equals e hashCode corretti per te, e i due rimarranno sempre sincronizzati al variare dei campi. Se non puoi usare un record, il comando "genera equals/hashCode" del tuo IDE è la migliore alternativa.
Un esempio pratico
Cosa c'è dopo
equals consente ai tuoi oggetti di confrontarsi tra loro; toString consente loro di descriversi. Il prossimo capitolo parla di come fare l'override di toString per produrre un output davvero utile nei log, nei messaggi di errore e nei debugger. Continua con Il metodo toString di Java.