Il contratto di equals/hashCode
Perché devono essere coerenti
Il contratto di equals/hashCode è una lezione Java Academy gratuita su CoddyKit. Questa è la lezione 2 di 4. Puoi leggere la lezione completa qui gratuitamente — poi esercitati direttamente nel browser con un editor di codice integrato e un tutor IA disponibile 24/7. Fa parte del percorso di apprendimento Java Academy, e i tuoi progressi si sincronizzano tra il web e l'app CoddyKit. Il corso Java Academy include 4 lezioni in totale.
Due metodi, un contratto
Ogni oggetto Java eredita equals() e hashCode() da Object.
Quando esegue l'override di uno dei due, quasi sempre deve eseguire l'override anche dell'altro. Insieme formano un contratto vincolante, da cui dipendono le collezioni basate su hash.
public class Main {
public static void main(String[] args) {
Object a = new Object();
Object b = new Object();
System.out.println(a.equals(b));
System.out.println(a.hashCode() == b.hashCode());
}
}La regola fondamentale
Il contratto stabilisce quanto segue:
- Se
a.equals(b)è true, alloraa.hashCode()deve essere uguale ab.hashCode(). - Se i codici hash sono diversi, gli oggetti sono certamente diversi.
Gli oggetti uguali devono avere codici hash uguali. Non è necessario il contrario.
public class Main {
public static void main(String[] args) {
String s1 = "hello";
String s2 = "hel" + "lo";
System.out.println(s1.equals(s2));
System.out.println(s1.hashCode() == s2.hashCode());
}
}Oggetti diversi possono condividere un hash
È consentito che due oggetti diversi condividano lo stesso codice hash. Si tratta semplicemente di una collisione, che la mappa gestisce con equals().
L'unica situazione vietata è quindi questa: oggetti uguali con codici hash diversi.
public class Main {
public static void main(String[] args) {
// Equal value, unequal objects, same hash is fine
System.out.println("FB".hashCode() == "Ea".hashCode());
System.out.println("FB".equals("Ea"));
}
}Una classe che esegue l'override solo di equals
Ecco una classe non corretta. Esegue l'override di equals(), ma non di hashCode().
Ora due punti logicamente uguali hanno codici hash diversi, ereditati da Object, violando il contratto.
public class Main {
static class Point {
final int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
@Override public boolean equals(Object o) {
if (!(o instanceof Point p)) return false;
return x == p.x && y == p.y;
}
// BUG: no hashCode override
}
public static void main(String[] args) {
Point a = new Point(1, 2);
Point b = new Point(1, 2);
System.out.println("equals: " + a.equals(b));
System.out.println("same hash: " + (a.hashCode() == b.hashCode()));
}
}Il bug in un HashSet
La classe non corretta non funziona in un HashSet. L'insieme cerca nel bucket scelto dall'hash errato e non trova mai l'elemento uguale.
Il risultato sono duplicati che avrebbero dovuto essere rifiutati.
import java.util.HashSet;
import java.util.Set;
public class Main {
static class Point {
final int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
@Override public boolean equals(Object o) {
if (!(o instanceof Point p)) return false;
return x == p.x && y == p.y;
}
}
public static void main(String[] args) {
Set<Point> set = new HashSet<>();
set.add(new Point(1, 2));
set.add(new Point(1, 2));
System.out.println("size = " + set.size());
}
}Correzione
Esegua l'override di entrambi i metodi e li derivi dagli stessi campi. In questo modo gli oggetti uguali condividono lo stesso hash e finiscono nello stesso bucket.
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
public class Main {
static class Point {
final int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
@Override public boolean equals(Object o) {
if (!(o instanceof Point p)) return false;
return x == p.x && y == p.y;
}
@Override public int hashCode() { return Objects.hash(x, y); }
}
public static void main(String[] args) {
Set<Point> set = new HashSet<>();
set.add(new Point(1, 2));
set.add(new Point(1, 2));
System.out.println("size = " + set.size());
}
}equals deve essere riflessivo
Un equals() corretto rispetta quattro proprietà. La prima è la riflessività: x.equals(x) deve essere true.
Può sembrare ovvio, ma una logica personalizzata può violare questa proprietà se si dimentica di gestire il caso dello stesso riferimento.
public class Main {
public static void main(String[] args) {
String x = "java";
System.out.println(x.equals(x));
}
}Simmetrico e transitivo
Altre due regole:
- Simmetrico: se
a.equals(b), allorab.equals(a). - Transitivo: se
a.equals(b)eb.equals(c), alloraa.equals(c).
Mescolare tipi diversi all'interno di una gerarchia di classi spesso viola la simmetria, quindi confronti le classi con attenzione.
public class Main {
public static void main(String[] args) {
String a = "x", b = "x", c = "x";
boolean symmetric = a.equals(b) == b.equals(a);
boolean transitive = a.equals(b) && b.equals(c) && a.equals(c);
System.out.println("symmetric: " + symmetric);
System.out.println("transitive: " + transitive);
}
}Coerente
La quarta regola è la coerenza: chiamate ripetute devono restituire lo stesso risultato finché gli oggetti non cambiano.
Per questo dovrebbe basare equals e hashCode su campi immutabili. Modificare dopo l'inserimento un campo usato da hashCode danneggia la mappa.
import java.util.Objects;
public class Main {
public static void main(String[] args) {
// Immutable record gives consistent equals/hashCode automatically
record Id(int value) {}
Id id = new Id(7);
System.out.println(Objects.equals(id, new Id(7)));
System.out.println(id.hashCode() == new Id(7).hashCode());
}
}I record lo fanno automaticamente
Un record Java genera automaticamente equals() e hashCode() a partire da tutti i suoi componenti.
Per i dati simili a valori, preferisca i record. Il contratto viene rispettato automaticamente e rimane sincronizzato.
import java.util.HashSet;
import java.util.Set;
public class Main {
record Point(int x, int y) {}
public static void main(String[] args) {
Set<Point> set = new HashSet<>();
set.add(new Point(1, 2));
set.add(new Point(1, 2));
System.out.println("size = " + set.size());
}
}La trappola delle chiavi mutabili
Se modifica un campo usato da hashCode dopo aver inserito l'oggetto in una mappa, non potrà più trovarlo.
La mappa cerca nel vecchio bucket, mentre ora l'oggetto produce l'hash di uno nuovo. L'entry diventa un fantasma perduto.
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
public class Main {
static class Box {
int id;
Box(int id) { this.id = id; }
@Override public boolean equals(Object o) {
return o instanceof Box b && b.id == id;
}
@Override public int hashCode() { return Objects.hash(id); }
}
public static void main(String[] args) {
Map<Box, String> m = new HashMap<>();
Box key = new Box(1);
m.put(key, "value");
key.id = 99; // mutated after insertion
System.out.println(m.get(key));
}
}Verifica rapida
Verifichi la Sua comprensione del contratto.
Riepilogo
Ha imparato il contratto di equals/hashCode:
- Gli oggetti uguali devono avere codici hash uguali.
- equals deve essere riflessivo, simmetrico, transitivo e coerente.
- Basare entrambi i metodi sugli stessi campi immutabili.
- I record generano automaticamente implementazioni corrette.
Successivamente scriverà manualmente il Suo hashCode corretto.
import java.util.Objects;
public class Main {
public static void main(String[] args) {
System.out.println("Contract recap: " + Objects.hash(1, 2, 3));
}
}Domande Frequenti
La lezione «Il contratto di equals/hashCode» è gratuita?
Sì — il testo completo di «Il contratto di equals/hashCode» è gratuito qui sul web. Per esercitarvi in modo interattivo (un editor di codice integrato e un tutor IA 24/7) e sbloccare il resto del corso Java Academy, passa a CoddyKit PRO. Il corso Java Academy include 4 lezioni in totale.
Cosa imparerò in «Il contratto di equals/hashCode»?
Perché devono essere coerenti Eserciti Java Academy con codice pratico che esegui direttamente nel browser, e un tutor IA 24/7 risponde alle tue domande mentre lavori sulla lezione.
Ho bisogno di esperienza per iniziare Java Academy?
Non è richiesta alcuna esperienza precedente. Java Academy su CoddyKit è strutturato per principianti e studenti avanzati, quindi puoi iniziare da qui o dall'inizio e procedere al tuo ritmo. Questa è la lezione 2 di 4.
Quanto tempo richiede la lezione «Il contratto di equals/hashCode»?
La maggior parte delle lezioni CoddyKit richiede circa 5–10 minuti. Ogni lezione è breve e interattiva, quindi fai progressi costanti e riprendi esattamente da dove hai lasciato su web e app.
Posso scrivere ed eseguire codice in questa lezione Java Academy?
Sì. Ogni lezione Java Academy include un editor di codice integrato, quindi scrivi ed esegui codice reale direttamente nel tuo browser e ricevi feedback istantaneo dall'IA — nessuna configurazione locale necessaria.
Tutte le lezioni di questo corso
- Come funziona HashMap
- Il contratto di equals/hashCode
- Implementare hashCode
- Treeification e prestazioni