CodeGym /Java Blog /Random-IT /metodi equals e hashCode: best practice
John Squirrels
Livello 41
San Francisco

metodi equals e hashCode: best practice

Pubblicato nel gruppo Random-IT
CIAO! Oggi parleremo di due metodi importanti in Java: equals()e hashCode(). Non è la prima volta che li incontriamo: il corso CodeGym inizia con una breve lezione su equals()— leggila se l'hai dimenticata o non l'hai mai vista prima... metodi equals e hashCode: best practices - 1Nella lezione di oggi parleremo di questi concetti in dettaglio. E credimi, abbiamo qualcosa di cui parlare! Ma prima di passare al nuovo, aggiorniamo ciò che abbiamo già trattato :) Come ricorderai, di solito è una cattiva idea confrontare due oggetti usando l' ==operatore, perché ==confronta i riferimenti. Ecco il nostro esempio con le auto da una lezione recente:

public class Car {

   String model;
   int maxSpeed;

   public static void main(String[] args) {

       Car car1 = new Car();
       car1.model = "Ferrari";
       car1.maxSpeed = 300;

       Car car2 = new Car();
       car2.model = "Ferrari";
       car2.maxSpeed = 300;

       System.out.println(car1 == car2);
   }
}
Uscita console:

false
Sembra che abbiamo creato due Caroggetti identici: i valori dei campi corrispondenti dei due oggetti auto sono gli stessi, ma il risultato del confronto è ancora falso. Conosciamo già il motivo: i riferimenti car1e car2puntano a indirizzi di memoria diversi, quindi non sono uguali. Ma vogliamo comunque confrontare i due oggetti, non due riferimenti. La migliore soluzione per confrontare gli oggetti è il equals()metodo.

metodo uguale()

Ricorderete che non creiamo questo metodo da zero, piuttosto lo sovrascriviamo: il equals()metodo è definito nella Objectclasse. Detto questo, nella sua forma abituale, è di scarsa utilità:

public boolean equals(Object obj) {
   return (this == obj);
}
Ecco come equals()viene definito il metodo nella Objectclasse. Questo è ancora una volta un confronto di riferimenti. Perché l'hanno fatto così? Bene, come fanno i creatori del linguaggio a sapere quali oggetti nel tuo programma sono considerati uguali e quali no? :) Questo è il punto principale del equals()metodo: il creatore di una classe è colui che determina quali caratteristiche vengono utilizzate durante il controllo dell'uguaglianza degli oggetti della classe. Quindi sovrascrivi il equals()metodo nella tua classe. Se non capisci bene il significato di "determina quali caratteristiche", consideriamo un esempio. Ecco una semplice classe che rappresenta un uomo: Man.

public class Man {

   private String noseSize;
   private String eyesColor;
   private String haircut;
   private boolean scars;
   private int dnaCode;

public Man(String noseSize, String eyesColor, String haircut, boolean scars, int dnaCode) {
   this.noseSize = noseSize;
   this.eyesColor = eyesColor;
   this.haircut = haircut;
   this.scars = scars;
   this.dnaCode = dnaCode;
}

   // Getters, setters, etc.
}
Supponiamo di scrivere un programma che deve determinare se due persone sono gemelle identiche o semplicemente simili. Abbiamo cinque caratteristiche: dimensione del naso, colore degli occhi, acconciatura, presenza di cicatrici e risultati del test del DNA (per semplicità, lo rappresentiamo come un codice intero). Quale di queste caratteristiche pensi consentirebbe al nostro programma di identificare gemelli identici? metodi equals e hashCode: best practices - 2Naturalmente, solo un test del DNA può fornire una garanzia. Due persone possono avere lo stesso colore degli occhi, taglio di capelli, naso e persino cicatrici: ci sono molte persone al mondo ed è impossibile garantire che non ci siano doppelgänger là fuori. Ma abbiamo bisogno di un meccanismo affidabile: solo il risultato di un test del DNA ci permetterà di trarre una conclusione precisa. Cosa significa questo per il nostro equals()metodo? Dobbiamo sovrascriverlo nel fileManclasse, tenendo conto dei requisiti del nostro programma. Il metodo dovrebbe confrontare il int dnaCodecampo dei due oggetti. Se sono uguali, allora gli oggetti sono uguali.

@Override
public boolean equals(Object o) {
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
è davvero così semplice? Non proprio. Abbiamo trascurato qualcosa. Per i nostri oggetti, abbiamo identificato solo un campo rilevante per stabilire l'uguaglianza degli oggetti: dnaCode. Ora immagina di avere non 1, ma 50 campi rilevanti. E se tutti i 50 campi di due oggetti sono uguali, allora gli oggetti sono uguali. Anche uno scenario del genere è possibile. Il problema principale è che stabilire l'uguaglianza confrontando 50 campi è un processo che richiede tempo e risorse. Ora immagina che oltre alla nostra Manclasse, abbiamo una Womanclasse con esattamente gli stessi campi che esistono in Man. Se un altro programmatore utilizza le nostre classi, potrebbe facilmente scrivere codice come questo:

public static void main(String[] args) {
  
   Man man = new Man(........); // A bunch of parameters in the constructor

   Woman woman = new Woman(.........); // The same bunch of parameters.

   System.out.println(man.equals(woman));
}
In questo caso, controllare i valori del campo è inutile: possiamo facilmente vedere che abbiamo oggetti di due classi diverse, quindi non c'è modo che possano essere uguali! Ciò significa che dovremmo aggiungere un controllo al equals()metodo, confrontando le classi degli oggetti confrontati. È un bene che ci abbiamo pensato!

@Override
public boolean equals(Object o) {
   if (getClass() != o.getClass()) return false;
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
Ma forse abbiamo dimenticato qualcos'altro? Hmm... Come minimo, dovremmo controllare che non stiamo confrontando un oggetto con se stesso! Se i riferimenti A e B puntano allo stesso indirizzo di memoria, allora sono lo stesso oggetto e non abbiamo bisogno di perdere tempo e confrontare 50 campi.

@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (getClass() != o.getClass()) return false;
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
Inoltre non fa male aggiungere un segno di spunta per null: nessun oggetto può essere uguale a null. Quindi, se il parametro del metodo è nullo, non ha senso eseguire ulteriori controlli. Tenendo presente tutto ciò, il nostro equals()metodo per la Manclasse è simile al seguente:

@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (o == null || getClass() != o.getClass()) return false;
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
Eseguiamo tutti i controlli iniziali sopra menzionati. Alla fine della giornata, se:
  • stiamo confrontando due oggetti della stessa classe
  • e gli oggetti confrontati non sono lo stesso oggetto
  • e l'oggetto passato non lo ènull
...poi si procede al confronto delle relative caratteristiche. Per noi questo significa i dnaCodecampi dei due oggetti. Quando si esegue l'override del equals()metodo, accertarsi di rispettare questi requisiti:
  1. Riflessività.

    Quando il equals()metodo viene utilizzato per confrontare qualsiasi oggetto con se stesso, deve restituire true.
    Abbiamo già rispettato questo requisito. Il nostro metodo prevede:

    
    if (this == o) return true;
    

  2. Simmetria.

    Se a.equals(b) == true, allora b.equals(a)deve tornare true.
    Il nostro metodo soddisfa anche questo requisito.

  3. Transitività.

    Se due oggetti sono uguali a un terzo oggetto, allora devono essere uguali tra loro.
    Se a.equals(b) == truee a.equals(c) == true, anche allora b.equals(c)deve restituire vero.

  4. Persistenza.

    Il risultato di equals()deve cambiare solo quando vengono modificati i campi interessati. Se i dati dei due oggetti non cambiano, il risultato di equals()deve essere sempre lo stesso.

  5. Disuguaglianza con null.

    Per qualsiasi oggetto, a.equals(null)deve restituire false
    Questo non è solo un insieme di alcune "raccomandazioni utili", ma piuttosto un contratto rigoroso , stabilito nella documentazione di Oracle

metodo hashCode()

Ora parliamo del hashCode()metodo. Perché è necessario? Esattamente per lo stesso scopo: confrontare gli oggetti. Ma abbiamo già equals()! Perché un altro metodo? La risposta è semplice: migliorare le prestazioni. Una funzione hash, rappresentata in Java utilizzando il hashCode()metodo, restituisce un valore numerico di lunghezza fissa per qualsiasi oggetto. In Java, il hashCode()metodo restituisce un numero a 32 bit ( int) per qualsiasi oggetto. Il confronto di due numeri è molto più rapido rispetto al confronto di due oggetti utilizzando il equals()metodo, soprattutto se tale metodo considera molti campi. Se il nostro programma confronta gli oggetti, questo è molto più semplice da fare usando un codice hash. Solo se gli oggetti sono uguali in base al hashCode()metodo, il confronto procede alequals()metodo. A proposito, è così che funzionano le strutture dati basate su hash, ad esempio il familiare HashMap! Il hashCode()metodo, come il equals()metodo, viene sovrascritto dallo sviluppatore. E proprio come equals(), il hashCode()metodo ha requisiti ufficiali enunciati nella documentazione di Oracle:
  1. Se due oggetti sono uguali (cioè il equals()metodo restituisce true), allora devono avere lo stesso codice hash.

    Altrimenti, i nostri metodi sarebbero privi di significato. Come accennato in precedenza, un hashCode()controllo dovrebbe andare prima per migliorare le prestazioni. Se i codici hash fossero diversi, il controllo restituirebbe false, anche se gli oggetti sono effettivamente uguali in base a come abbiamo definito il equals()metodo.

  2. Se il hashCode()metodo viene chiamato più volte sullo stesso oggetto, deve restituire ogni volta lo stesso numero.

  3. La regola 1 non funziona nella direzione opposta. Due oggetti diversi possono avere lo stesso codice hash.

La terza regola è un po' confusa. Come può essere? La spiegazione è abbastanza semplice. Il hashCode()metodo restituisce un int. An intè un numero a 32 bit. Ha un range di valori limitato: da -2.147.483.648 a +2.147.483.647. In altre parole, ci sono poco più di 4 miliardi di possibili valori per un int. Ora immagina di creare un programma per archiviare dati su tutte le persone che vivono sulla Terra. Ogni persona corrisponderà al proprio Personoggetto (simile alla Manclasse). Ci sono circa 7,5 miliardi di persone che vivono sul pianeta. In altre parole, non importa quanto sia intelligente l'algoritmo che scriviamo per la conversionePersonoggetti a un int, semplicemente non abbiamo abbastanza numeri possibili. Abbiamo solo 4,5 miliardi di possibili valori int, ma ci sono molte più persone di così. Ciò significa che non importa quanto ci sforziamo, alcune persone diverse avranno gli stessi codici hash. Quando ciò accade (i codici hash coincidono per due oggetti diversi) la chiamiamo collisione. Quando si esegue l'override del hashCode()metodo, uno degli obiettivi del programmatore è ridurre al minimo il numero potenziale di collisioni. Tenendo conto di tutte queste regole, come sarà il hashCode()metodo in Personclasse? Come questo:

@Override
public int hashCode() {
   return dnaCode;
}
Sorpreso? :) Se guardi i requisiti, vedrai che li rispettiamo tutti. Anche gli oggetti per i quali il nostro equals()metodo restituisce true saranno uguali secondo hashCode(). Se i nostri due Personoggetti sono uguali in equals(cioè hanno lo stesso dnaCode), allora il nostro metodo restituisce lo stesso numero. Consideriamo un esempio più difficile. Supponiamo che il nostro programma selezioni auto di lusso per collezionisti di automobili. Il collezionismo può essere un hobby complesso con molte particolarità. Una particolare auto del 1963 può costare 100 volte di più di un'auto del 1964. Un'auto rossa del 1970 può costare 100 volte di più di un'auto blu della stessa marca e dello stesso anno. metodi equals e hashCode: best practices - 4Nel nostro esempio precedente, con la Personclasse, abbiamo scartato la maggior parte dei campi (cioè le caratteristiche umane) come insignificanti e abbiamo usato solo thednaCodecampo nei confronti. Ora stiamo lavorando in un regno molto idiosincratico, in cui non ci sono dettagli insignificanti! Ecco la nostra LuxuryAutoclasse:

public class LuxuryAuto {

   private String model;
   private int manufactureYear;
   private int dollarPrice;

   public LuxuryAuto(String model, int manufactureYear, int dollarPrice) {
       this.model = model;
       this.manufactureYear = manufactureYear;
       this.dollarPrice = dollarPrice;
   }

   // ...getters, setters, etc.
}
Ora dobbiamo considerare tutti i campi nei nostri confronti. Qualsiasi errore potrebbe costare a un cliente centinaia di migliaia di dollari, quindi sarebbe meglio essere troppo prudenti:

@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (o == null || getClass() != o.getClass()) return false;

   LuxuryAuto that = (LuxuryAuto) o;

   if (manufactureYear != that.manufactureYear) return false;
   if (dollarPrice != that.dollarPrice) return false;
   return model.equals(that.model);
}
Nel nostro equals()metodo non abbiamo dimenticato tutti i controlli di cui abbiamo parlato prima. Ma ora confrontiamo ciascuno dei tre campi dei nostri oggetti. Per questo programma, abbiamo bisogno dell'uguaglianza assoluta, cioè dell'uguaglianza di ogni campo. Che dire hashCode?

@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = result + manufactureYear;
   result = result + dollarPrice;
   return result;
}
Il modelcampo nella nostra classe è una stringa. Questo è conveniente, perché la Stringclasse sovrascrive già il hashCode()metodo. Calcoliamo il modelcodice hash del campo e quindi vi aggiungiamo la somma degli altri due campi numerici. Gli sviluppatori Java hanno un semplice trucco che usano per ridurre il numero di collisioni: quando calcolano un codice hash, moltiplicano il risultato intermedio per un numero primo dispari. Il numero più comunemente usato è 29 o 31. Non approfondiremo le sottigliezze matematiche in questo momento, ma in futuro ricorderemo che moltiplicare i risultati intermedi per un numero dispari sufficientemente grande aiuta a "distribuire" i risultati della funzione hash e, di conseguenza, ridurre il numero di oggetti con lo stesso codice hash. Per il nostro hashCode()metodo in LuxuryAuto, sarebbe simile a questo:

@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = 31 * result + manufactureYear;
   result = 31 * result + dollarPrice;
   return result;
}
Puoi leggere di più su tutte le complessità di questo meccanismo in questo post su StackOverflow , così come nel libro Effective Java di Joshua Bloch. Infine, un altro punto importante che vale la pena menzionare. Ogni volta che sovrascriviamo il metodo equals()and hashCode(), selezioniamo determinati campi di istanza che vengono presi in considerazione in questi metodi. Questi metodi considerano gli stessi campi. Ma possiamo considerare diversi campi in equals()e hashCode()? Tecnicamente, possiamo. Ma questa è una cattiva idea, ed ecco perché:

@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (o == null || getClass() != o.getClass()) return false;

   LuxuryAuto that = (LuxuryAuto) o;

   if (manufactureYear != that.manufactureYear) return false;
   return dollarPrice == that.dollarPrice;
}

@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = 31 * result + manufactureYear;
   result = 31 * result + dollarPrice;
   return result;
}
Ecco i nostri metodi equals()e hashCode()per la LuxuryAutoclasse. Il hashCode()metodo è rimasto invariato, ma abbiamo rimosso il modelcampo dal equals()metodo. Il modello non è più una caratteristica utilizzata quando il equals()metodo confronta due oggetti. Ma quando si calcola il codice hash, quel campo viene comunque preso in considerazione. Cosa otteniamo come risultato? Creiamo due auto e scopriamolo!

public class Main {

   public static void main(String[] args) {

       LuxuryAuto ferrariGTO = new LuxuryAuto("Ferrari 250 GTO", 1963, 70000000);
       LuxuryAuto ferrariSpider = new LuxuryAuto("Ferrari 335 S Spider Scaglietti", 1963, 70000000);

       System.out.println("Are these two objects equal to each other?");
       System.out.println(ferrariGTO.equals(ferrariSpider));

       System.out.println("What are their hash codes?");
       System.out.println(ferrariGTO.hashCode());
       System.out.println(ferrariSpider.hashCode());
   }
}

Are these two objects equal to each other? 
true 
What are their hash codes? 
-1372326051 
1668702472
Errore! Utilizzando campi diversi per equals()e hashCode()metodi, abbiamo violato i contratti che sono stati stabiliti per loro! Due oggetti uguali secondo il equals()metodo devono avere lo stesso codice hash. Abbiamo ricevuto valori diversi per loro. Tali errori possono portare a conseguenze assolutamente incredibili, soprattutto quando si lavora con raccolte che utilizzano un hash. Di conseguenza, quando si esegue l'override equals()e hashCode(), è necessario considerare gli stessi campi. Questa lezione è stata piuttosto lunga, ma hai imparato molto oggi! :) Ora è il momento di tornare a risolvere compiti!
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION