W3docs

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:

  • Riflessivox.equals(x) è true.
  • Simmetricox.equals(y) se e solo se y.equals(x).
  • Transitivo — se x.equals(y) e y.equals(z), allora x.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:

java— editable, runs on the server

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 == o gestisce velocemente il caso comune.
  • Controllo del tipo con binding. instanceof Point p rifiuta 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.compare per 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:

  • instanceof consente 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

java— editable, runs on the server

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.

Esercitati

Pratica
Perché è un bug fare l'override di `equals` senza fare anche l'override di `hashCode`?
Perché è un bug fare l'override di `equals` senza fare anche l'override di `hashCode`?
Was this page helpful?