JavaRush /Java Blog /Random-IT /Metodi equals e hashCode: pratica d'uso

Metodi equals e hashCode: pratica d'uso

Pubblicato nel gruppo Random-IT
Ciao! Oggi parleremo di due metodi importanti in Java: equals()e hashCode(). Non è la prima volta che li incontriamo: all'inizio del corso JavaRush c'era una breve lezione sul tema equals(): leggila se l'hai dimenticata o non l'hai vista prima. Metodi è uguale a &  hashCode: pratica d'uso - 1Nella lezione di oggi parleremo nel dettaglio di questi concetti: credetemi, c’è molto di cui parlare! E prima di passare a qualcosa di nuovo, rinfreschiamo la memoria su ciò che abbiamo già trattato :) Come ricorderete, il solito confronto di due oggetti utilizzando l' ==operatore “ ” è una cattiva idea, perché “ ==” confronta i riferimenti. Ecco il nostro esempio con le auto da una recente conferenza:
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
Sembrerebbe che abbiamo creato due oggetti identici della classe Car: tutti i campi sulle due macchine sono uguali, ma il risultato del confronto è ancora falso. Conosciamo già il motivo: i collegamenti car1puntano car2a indirizzi diversi in memoria, quindi non sono uguali. Vogliamo ancora confrontare due oggetti, non due riferimenti. La soluzione migliore per confrontare oggetti è il file equals().

metodo equals()

Potresti ricordare che non creiamo questo metodo da zero, ma lo sovrascriviamo: dopo tutto, il metodo equals()è definito nella classe Object. Tuttavia, nella sua forma abituale è di scarsa utilità:
public boolean equals(Object obj) {
   return (this == obj);
}
Ecco come equals()viene definito il metodo nella classe Object. Lo stesso confronto di collegamenti. Perché è stato fatto così? Ebbene, come fanno i creatori del linguaggio a sapere quali oggetti nel tuo programma sono considerati uguali e quali no? :) Questa è l'idea principale del metodo equals(): il creatore della classe stesso determina le caratteristiche con cui viene verificata l'uguaglianza degli oggetti di questa classe. In questo modo, sovrascrivi il metodo equals()nella tua classe. Se non capisci bene il significato di “definisci tu stesso le caratteristiche”, diamo un’occhiata a un esempio. Ecco una semplice classe di persone - 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.
}
Diciamo che stiamo scrivendo un programma che deve determinare se due persone sono imparentate tramite gemelli o semplicemente doppelgänger. Abbiamo cinque caratteristiche: dimensione del naso, colore degli occhi, acconciatura, presenza di cicatrici e risultati di un test biologico del DNA (per semplicità - sotto forma di un numero di codice). Quali di queste caratteristiche pensi che consentiranno al nostro programma di identificare i parenti gemelli? Metodi è uguale a &  hashCode: pratica d'uso - 2Naturalmente solo un test biologico può fornire una garanzia. Due persone possono avere lo stesso colore degli occhi, acconciatura, naso e persino cicatrici: ci sono molte persone al mondo ed è impossibile evitare coincidenze. Abbiamo bisogno di un meccanismo affidabile: solo il risultato di un test del DNA ci permette di trarre una conclusione accurata. Cosa significa questo per il nostro metodo equals()? Dobbiamo ridefinirlo in una classe Mantenendo conto dei requisiti del nostro programma. Il metodo deve confrontare il campo di int dnaCodedue oggetti e, 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. Ci siamo persi qualcosa. In questo caso, per i nostri oggetti abbiamo definito solo un campo “significativo” attraverso il quale viene stabilita la loro uguaglianza: dnaCode. Ora immagina di avere non 1, ma 50 campi "significativi" e se tutti i 50 campi di due oggetti sono uguali, allora gli oggetti sono uguali. Potrebbe anche succedere questo. Il problema principale è che il calcolo dell'uguaglianza di 50 campi è un processo lungo e dispendioso in termini di risorse. Ora immagina che oltre alla classe, Manabbiamo una classe Womancon esattamente gli stessi campi di Man. E se un altro programmatore utilizza le tue classi, può facilmente scrivere nel suo programma qualcosa del tipo:
public static void main(String[] args) {

   Man man = new Man(........); //a bunch of parameters in the constructor

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

   System.out.println(man.equals(woman));
}
In questo caso non ha senso controllare i valori dei campi: vediamo che stiamo guardando oggetti di due classi diverse e in linea di principio non possono essere uguali! Ciò significa che dobbiamo effettuare un controllo nel metodo equals(): un confronto tra oggetti di due classi identiche. È un bene che abbiamo pensato a questo!
@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 l'oggetto con se stesso! Se i riferimenti A e B puntano allo stesso indirizzo in memoria, allora sono lo stesso oggetto e non abbiamo bisogno di perdere tempo confrontando 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 sarebbe male aggiungere un controllo per null: nessun oggetto può essere uguale a null, nel qual caso non ha senso effettuare controlli aggiuntivi. Tenendo conto di tutto ciò, il nostro metodo equals()di classe Mansarà 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;
}
Effettuiamo tutti i controlli iniziali sopra menzionati. Se risulta che:
  • confrontiamo due oggetti della stessa classe
  • questo non è lo stesso oggetto
  • non stiamo confrontando il nostro oggetto connull
...poi passiamo al confronto delle caratteristiche significative. Nel nostro caso, i campi dnaCodedi due oggetti. Quando esegui l'override di un metodo equals(), assicurati di rispettare questi requisiti:
  1. Riflessività.

    Qualsiasi oggetto deve essere equals()per se stesso.
    Abbiamo già tenuto conto di questa esigenza. Il nostro metodo prevede:

    if (this == o) return true;

  2. Simmetria.

    Se a.equals(b) == true, allora b.equals(a)dovrebbe restituire 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 il controllo b.equals(c)dovrebbe restituire true.

  4. Permanenza.

    I risultati del lavoro equals()dovrebbero cambiare solo quando cambiano i campi in esso inclusi. Se i dati di due oggetti non sono cambiati, i risultati del controllo equals()dovrebbero essere sempre gli stessi.

  5. Disuguaglianza con null.

    Per qualsiasi oggetto il controllo a.equals(null)deve restituire false,
    che non è solo un insieme di alcune “raccomandazioni utili”, ma un rigido contratto di metodi , prescritti nella documentazione Oracle

metodo hashCode()

Ora parliamo del metodo hashCode(). Perché è necessario? Esattamente per lo stesso scopo: confrontare oggetti. Ma ce l'abbiamo già equals()! Perché un altro metodo? La risposta è semplice: migliorare la produttività. Una funzione hash, rappresentata dal metodo , in Java hashCode(), restituisce un valore numerico a lunghezza fissa per qualsiasi oggetto. Nel caso di Java, il metodo hashCode()restituisce un numero a 32 bit di tipo int. Confrontare due numeri tra loro è molto più veloce che confrontare due oggetti utilizzando il metodo equals(), soprattutto se utilizza molti campi. Se il nostro programma confronterà gli oggetti, è molto più semplice farlo tramite codice hash e solo se sono uguali per hashCode()procedere al confronto tramite equals(). Questo è, tra l'altro, come funzionano le strutture dati basate su hash, ad esempio quella che conosci HashMap! Il metodo hashCode(), proprio come equals(), viene sovrascritto dallo sviluppatore stesso. E proprio come per equals(), il metodo hashCode()ha requisiti ufficiali specificati nella documentazione Oracle:
  1. Se due oggetti sono uguali (ovvero, il metodo equals()restituisce true), devono avere lo stesso codice hash.

    Altrimenti i nostri metodi non avranno senso. Il controllo tramite hashCode(), come abbiamo detto, dovrebbe venire prima per migliorare le prestazioni. Se i codici hash sono diversi, il controllo restituirà false, anche se gli oggetti sono effettivamente uguali (come abbiamo definito nel metodo equals()).

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

  3. La regola 1 non funziona al contrario. Due oggetti diversi possono avere lo stesso codice hash.

La terza regola è un po’ confusa. Come può essere? La spiegazione è abbastanza semplice. Il metodo hashCode()restituisce int. intè un numero a 32 bit. Ha un numero limitato di valori: da -2.147.483.648 a +2.147.483.647. In altre parole, ci sono poco più di 4 miliardi di variazioni del numero int. Ora immagina di creare un programma per archiviare dati su tutte le persone viventi sulla Terra. Ogni persona avrà il proprio oggetto di classe Man. Sulla terra vivono circa 7,5 miliardi di persone. In altre parole, non importa quanto sia buono l’algoritmo Manche scriviamo per convertire gli oggetti in numeri, semplicemente non avremo abbastanza numeri. Abbiamo solo 4,5 miliardi di opzioni e molte più persone. Ciò significa che non importa quanto ci proviamo, i codici hash saranno gli stessi per persone diverse. Questa situazione (i codici hash di due oggetti diversi corrispondenti) è chiamata collisione. Uno degli obiettivi del programmatore quando si sovrascrive un metodo hashCode()è ridurre il più possibile il numero potenziale di collisioni. Come sarà il nostro metodo hashCode()per la lezione Man, tenendo conto di tutte queste regole? Come questo:
@Override
public int hashCode() {
   return dnaCode;
}
Sorpreso? :) Inaspettatamente, ma se guardi i requisiti, vedrai che rispettiamo tutto. Gli oggetti per i quali il nostro equals()restituisce true saranno uguali in hashCode(). Se i nostri due oggetti Manhanno lo stesso valore equals(cioè hanno lo stesso valore dnaCode), il nostro metodo restituirà lo stesso numero. Consideriamo un esempio più complicato. Diciamo che il nostro programma dovrebbe selezionare auto di lusso per i clienti collezionisti. Collezionare è una cosa complessa e ha molte caratteristiche. Un’auto del 1963 può costare 100 volte di più della stessa auto del 1964. Un’auto rossa del 1970 può costare 100 volte di più di un’auto blu della stessa marca dello stesso anno. Metodi è uguale a &  hashCode: pratica d'uso - 4Nel primo caso, con la classe Man, abbiamo scartato la maggior parte dei campi (ad esempio, le caratteristiche della persona) in quanto insignificanti e abbiamo utilizzato solo il campo per il confronto dnaCode. Qui stiamo lavorando con un'area davvero unica e non possono esserci dettagli minori! Ecco la nostra classe LuxuryAuto:
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.
}
Qui, nel confronto, dobbiamo tenere conto di tutti i campi. Qualsiasi errore può costare centinaia di migliaia di dollari al cliente, quindi è meglio essere sicuri:
@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 metodo equals()non abbiamo dimenticato tutti i controlli di cui abbiamo parlato prima. Ma ora confrontiamo ciascuno dei tre campi dei nostri oggetti. In questo programma l’uguaglianza deve essere assoluta, in 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 campo modelnella nostra classe è una stringa. Questo è conveniente: Stringil metodo hashCode()è già sovrascritto nella classe. Calcoliamo il codice hash del campo model, e ad esso aggiungiamo la somma degli altri due campi numerici. C'è un piccolo trucco in Java che viene utilizzato per ridurre il numero di collisioni: quando si calcola il codice hash, moltiplicare il risultato intermedio per un numero primo dispari. Il numero più comunemente usato è 29 o 31. Non entreremo nei dettagli dei calcoli adesso, ma per riferimento futuro, ricorda che moltiplicare i risultati intermedi per un numero dispari sufficientemente grande aiuta a "diffondere" i risultati dell'hash funzione e ci ritroveremo con meno oggetti con lo stesso hashcode. Per il nostro metodo hashCode()in LuxuryAuto sarà 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 di Joshua Bloch “ Efficace Java ”. Infine, c’è un altro punto importante che vale la pena menzionare. Ogni volta durante l'override equals(), hashCode()abbiamo selezionato alcuni campi dell'oggetto, che sono stati presi in considerazione in questi metodi. Ma possiamo prendere in considerazione 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()per hashCode()la classe LuxuryAuto. Il metodo hashCode()è rimasto invariato e equals()abbiamo rimosso il campo dal metodo model. Ora il modello non è una caratteristica per confrontare due oggetti tramite equals(). Ma viene comunque preso in considerazione nel calcolo del codice hash. Cosa otterremo di conseguenza? Creiamo due auto e diamo un'occhiata!
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());
   }
}

Эти два an object равны друг другу?
true
Какие у них хэш-codeы?
-1372326051
1668702472
Errore! Utilizzando campi diversi per equals()e hashCode()abbiamo violato il contratto stabilito per loro! Due oggetti uguali equals()devono avere lo stesso codice hash. Abbiamo significati diversi per loro. Tali errori possono portare alle conseguenze più incredibili, soprattutto quando si lavora con raccolte che utilizzano hash. Pertanto, durante la ridefinizione equals(), hashCode()sarà corretto utilizzare gli stessi campi. La lezione si è rivelata piuttosto lunga, ma oggi hai imparato molte cose nuove! :) È ora di tornare a risolvere i problemi!
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION