CodeGym /Blog Java /Aleatoriu /metode equals și hashCode: cele mai bune practici
John Squirrels
Nivel
San Francisco

metode equals și hashCode: cele mai bune practici

Publicat în grup
Bună! Astăzi vom vorbi despre două metode importante în Java: equals()și hashCode(). Nu este prima dată când îi întâlnim: cursul CodeGym începe cu o scurtă lecție despre equals()— citește-o dacă ai uitat-o ​​sau nu ai văzut-o până acum... Metode equals și hashCode: cele mai bune practici - 1În lecția de astăzi, vom vorbi despre aceste concepte în detaliu. Și credeți-mă, avem despre ce să vorbim! Dar înainte de a trece la nou, să reîmprospătăm ceea ce am acoperit deja :) După cum vă amintiți, de obicei este o idee proastă să comparați două obiecte folosind operatorul, ==deoarece ==compară referințele. Iată exemplul nostru cu mașini dintr-o lecție recentă:

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);
   }
}
Ieșire din consolă:

false
Se pare că am creat două Carobiecte identice: valorile câmpurilor corespunzătoare celor două obiecte mașină sunt aceleași, dar rezultatul comparației este încă fals. Știm deja motivul: referințele car1și car2indică adrese de memorie diferite, deci nu sunt egale. Dar vrem totuși să comparăm cele două obiecte, nu două referințe. Cea mai bună soluție pentru compararea obiectelor este equals()metoda.

metoda equals().

Poate vă amintiți că nu creăm această metodă de la zero, ci o suprascriem: metoda equals()este definită în Objectclasă. Acestea fiind spuse, în forma sa obișnuită, este de puțin folos:

public boolean equals(Object obj) {
   return (this == obj);
}
Așa equals()este definită metoda în Objectclasă. Aceasta este o comparație a referințelor încă o dată. De ce au făcut-o așa? Ei bine, de unde știu creatorii limbajului care obiecte din programul tău sunt considerate egale și care nu? :) Acesta este punctul principal al equals()metodei — creatorul unei clase este cel care determină ce caracteristici sunt folosite la verificarea egalității obiectelor clasei. Apoi suprascrieți equals()metoda din clasa dvs. Dacă nu înțelegeți prea bine sensul „determină care caracteristici”, să luăm în considerare un exemplu. Iată o clasă simplă reprezentând un bărbat: 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.
}
Să presupunem că scriem un program care trebuie să determine dacă doi oameni sunt gemeni identici sau pur și simplu seamănă. Avem cinci caracteristici: mărimea nasului, culoarea ochilor, stilul părului, prezența cicatricilor și rezultatele testelor ADN (pentru simplitate, reprezentăm acest lucru ca un cod întreg). Care dintre aceste caracteristici crezi că ar permite programului nostru să identifice gemeni identici? Metode equals și hashCode: cele mai bune practici - 2Desigur, doar un test ADN poate oferi o garanție. Doi oameni pot avea aceeași culoare a ochilor, tunsoare, nas și chiar cicatrici - există o mulțime de oameni în lume și este imposibil să garantezi că nu există doi doppelgängers acolo. Dar avem nevoie de un mecanism de încredere: doar rezultatul unui test ADN ne va permite să tragem o concluzie exactă. Ce înseamnă asta pentru equals()metoda noastră? Trebuie să-l depășim înManclasa, ținând cont de cerințele programului nostru. Metoda ar trebui să compare int dnaCodecâmpul celor două obiecte. Dacă sunt egale, atunci obiectele sunt egale.

@Override
public boolean equals(Object o) {
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
Este chiar atât de simplu? Nu chiar. Am trecut cu vederea ceva. Pentru obiectele noastre, am identificat un singur câmp care este relevant pentru stabilirea egalității obiectelor: dnaCode. Acum imaginați-vă că nu avem 1, ci 50 de câmpuri relevante. Și dacă toate cele 50 de câmpuri a două obiecte sunt egale, atunci obiectele sunt egale. Este posibil și un astfel de scenariu. Problema principală este că stabilirea egalității prin compararea a 50 de domenii este un proces care consumă mult timp și necesită resurse. Acum imaginați-vă că, în plus față de Manclasa noastră, avem o Womanclasă cu exact aceleași câmpuri care există în Man. Dacă un alt programator folosește clasele noastre, el sau ea ar putea scrie cu ușurință cod ca acesta:

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));
}
În acest caz, verificarea valorilor câmpului este inutilă: putem vedea cu ușurință că avem obiecte din două clase diferite, deci nu există nicio modalitate ca acestea să fie egale! Aceasta înseamnă că ar trebui să adăugăm o verificare la equals()metodă, comparând clasele obiectelor comparate. E bine că ne-am gândit la asta!

@Override
public boolean equals(Object o) {
   if (getClass() != o.getClass()) return false;
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
Dar poate am uitat altceva? Hmm... Cel puțin, ar trebui să verificăm că nu comparăm un obiect cu el însuși! Dacă referințele A și B indică aceeași adresă de memorie, atunci sunt același obiect și nu trebuie să pierdem timpul și să comparăm 50 de câmpuri.

@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;
}
De asemenea, nu strica să adăugați o verificare pentru null: niciun obiect nu poate fi egal cu null. Deci, dacă parametrul metodei este nul, atunci nu are rost să verifici suplimentare. Având în vedere toate acestea, atunci equals()metoda noastră pentru Manclasă arată astfel:

@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;
}
Efectuăm toate verificările inițiale menționate mai sus. La sfârșitul zilei, dacă:
  • comparăm două obiecte din aceeași clasă
  • iar obiectele comparate nu sunt același obiect
  • iar obiectul trecut nu estenull
...apoi trecem la o comparație a caracteristicilor relevante. Pentru noi, aceasta înseamnă dnaCodecâmpurile celor două obiecte. Când anulați equals()metoda, asigurați-vă că respectați aceste cerințe:
  1. Reflexivitate.

    Când equals()metoda este folosită pentru a compara orice obiect cu el însuși, trebuie să returneze adevărat.
    Am respectat deja această cerință. Metoda noastră include:

    
    if (this == o) return true;
    

  2. Simetrie.

    Dacă a.equals(b) == true, atunci b.equals(a)trebuie să se întoarcă true.
    Metoda noastră satisface și această cerință.

  3. Tranzitivitatea.

    Dacă două obiecte sunt egale cu un al treilea obiect, atunci trebuie să fie egale între ele.
    Dacă a.equals(b) == trueși a.equals(c) == true, atunci b.equals(c)trebuie să returneze și adevărat.

  4. Persistenţă.

    Rezultatul equals()trebuie să se schimbe numai atunci când câmpurile implicate sunt modificate. Dacă datele celor două obiecte nu se modifică, atunci rezultatul equals()trebuie să fie întotdeauna același.

  5. Inegalitatea cu null.

    Pentru orice obiect, a.equals(null)trebuie să returneze false
    Acesta nu este doar un set de „recomandări utile”, ci mai degrabă un contract strict , stabilit în documentația Oracle

metoda hashCode().

Acum să vorbim despre hashCode()metodă. De ce este necesar? Exact în același scop - de a compara obiecte. Dar avem deja equals()! De ce altă metodă? Răspunsul este simplu: pentru a îmbunătăți performanța. O funcție hash, reprezentată în Java folosind hashCode()metoda, returnează o valoare numerică cu lungime fixă ​​pentru orice obiect. În Java, hashCode()metoda returnează un număr de 32 de biți ( int) pentru orice obiect. Compararea a două numere este mult mai rapidă decât compararea a două obiecte folosind equals()metoda, mai ales dacă acea metodă ia în considerare multe câmpuri. Dacă programul nostru compară obiecte, acest lucru este mult mai simplu de făcut folosind un cod hash. Numai dacă obiectele sunt egale pe baza hashCode()metodei, comparația trece laequals()metodă. Apropo, așa funcționează structurile de date bazate pe hash, de exemplu, familiarul HashMap! Metoda hashCode(), ca și equals()metoda, este suprascrisă de dezvoltator. Și la fel ca equals(), hashCode()metoda are cerințe oficiale precizate în documentația Oracle:
  1. Dacă două obiecte sunt egale (adică metoda equals()returnează true), atunci ele trebuie să aibă același cod hash.

    Altfel, metodele noastre ar fi lipsite de sens. După cum am menționat mai sus, hashCode()ar trebui să se efectueze mai întâi o verificare pentru a îmbunătăți performanța. Dacă codurile hash ar fi diferite, atunci verificarea ar returna false, chiar dacă obiectele sunt de fapt egale conform modului în care am definit metoda equals().

  2. Dacă hashCode()metoda este apelată de mai multe ori pe același obiect, trebuie să returneze același număr de fiecare dată.

  3. Regula 1 nu funcționează în direcția opusă. Două obiecte diferite pot avea același cod hash.

A treia regulă este puțin confuză. Cum poate fi aceasta? Explicația este destul de simplă. Metoda hashCode()returnează un int. An inteste un număr de 32 de biți. Are un interval limitat de valori: de la -2.147.483.648 la +2.147.483.647. Cu alte cuvinte, există puțin peste 4 miliarde de valori posibile pentru un int. Acum imaginați-vă că creați un program pentru a stoca date despre toți oamenii care trăiesc pe Pământ. Fiecare persoană va corespunde propriului Personobiect (asemănător clasei Man). Pe planetă trăiesc aproximativ 7,5 miliarde de oameni. Cu alte cuvinte, indiferent cât de inteligent ar fi algoritmul pe care îl scriem pentru conversiePersonobiecte la un int, pur și simplu nu avem suficiente numere posibile. Avem doar 4,5 miliarde de valori int posibile, dar sunt mult mai mulți oameni decât atât. Aceasta înseamnă că, indiferent cât de mult ne-am strădui, unii oameni diferiți vor avea aceleași coduri hash. Când se întâmplă acest lucru (codurile hash coincid pentru două obiecte diferite) o numim o coliziune. Când treceți peste hashCode()metoda, unul dintre obiectivele programatorului este de a minimiza numărul potențial de coliziuni. Luând în considerare toate aceste reguli, cum va hashCode()arăta metoda în Personclasă? Ca aceasta:

@Override
public int hashCode() {
   return dnaCode;
}
Uimit? :) Dacă te uiți la cerințe, vei vedea că le respectăm pe toate. Obiectele pentru care equals()metoda noastră returnează adevărat vor fi, de asemenea, egale conform hashCode(). Dacă cele două Personobiecte ale noastre sunt egale în equals(adică au același dnaCode), atunci metoda noastră returnează același număr. Să luăm în considerare un exemplu mai dificil. Să presupunem că programul nostru ar trebui să selecteze mașini de lux pentru colecționarii de mașini. Colectarea poate fi un hobby complex, cu multe particularități. O anumită mașină din 1963 poate costa de 100 de ori mai mult decât o mașină din 1964. O mașină roșie din 1970 poate costa de 100 de ori mai mult decât o mașină albastră de aceeași marcă a aceluiași an. Metode equals și hashCode: cele mai bune practici - 4În exemplul nostru anterior, cu Personclasa, am eliminat majoritatea câmpurilor (adică caracteristicile umane) ca fiind nesemnificative și am folosit doardnaCodedomeniu în comparații. Lucrăm acum într-un tărâm foarte idiosincratic, în care nu există detalii nesemnificative! Iată LuxuryAutoclasa noastră:

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.
}
Acum trebuie să luăm în considerare toate domeniile în comparațiile noastre. Orice greșeală ar putea costa un client sute de mii de dolari, așa că ar fi mai bine să fii în siguranță:

@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);
}
În equals()metoda noastră, nu am uitat toate verificările despre care am vorbit mai devreme. Dar acum comparăm fiecare dintre cele trei câmpuri ale obiectelor noastre. Pentru acest program avem nevoie de egalitate absolută, adică egalitatea fiecărui domeniu. Ce zici hashCode?

@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = result + manufactureYear;
   result = result + dollarPrice;
   return result;
}
Câmpul modeldin clasa noastră este un șir. Acest lucru este convenabil, deoarece Stringclasa deja suprascrie hashCode()metoda. Calculăm modelcodul hash al câmpului și apoi adăugăm la acesta suma celorlalte două câmpuri numerice. Dezvoltatorii Java au un truc simplu pe care îl folosesc pentru a reduce numărul de coliziuni: atunci când calculează un cod hash, înmulțiți rezultatul intermediar cu un prim impar. Numărul cel mai des folosit este 29 sau 31. Nu ne vom aprofunda în subtilitățile matematice chiar acum, dar în viitor amintiți-vă că înmulțirea rezultatelor intermediare cu un număr impar suficient de mare ajută la „împrăștierea” rezultatelor funcției hash și, în consecință, reduceți numărul de obiecte cu același cod hash. Pentru metoda noastră hashCode()din LuxuryAuto, ar arăta astfel:

@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = 31 * result + manufactureYear;
   result = 31 * result + dollarPrice;
   return result;
}
Puteți citi mai multe despre toate complexitățile acestui mecanism în această postare pe StackOverflow , precum și în cartea Effective Java de Joshua Bloch. În sfârșit, încă un punct important care merită menționat. De fiecare dată când am depășit metoda equals()și hashCode(), am selectat anumite câmpuri de instanță care sunt luate în considerare în aceste metode. Aceste metode iau în considerare aceleași câmpuri. Dar putem lua în considerare domenii diferite în equals()și hashCode()? Tehnic, putem. Dar aceasta este o idee proastă și iată de ce:

@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;
}
Iată metodele noastre equals()și hashCode()pentru LuxuryAutoclasă. Metoda hashCode()a rămas neschimbată, dar am eliminat modelcâmpul din equals()metodă. Modelul nu mai este o caracteristică folosită atunci când equals()metoda compară două obiecte. Dar atunci când se calculează codul hash, acel câmp este încă luat în considerare. Ce obținem ca rezultat? Să creăm două mașini și să aflăm!

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
Eroare! Folosind diferite câmpuri pentru equals()și hashCode()metode, am încălcat contractele care au fost stabilite pentru ei! Două obiecte care sunt egale conform equals()metodei trebuie să aibă același cod hash. Am primit diferite valori pentru ei. Astfel de erori pot duce la consecințe absolut incredibile, mai ales atunci când lucrați cu colecții care folosesc un hash. Ca rezultat, atunci când înlocuiți equals()și hashCode(), ar trebui să luați în considerare aceleași câmpuri. Această lecție a fost destul de lungă, dar ai învățat multe astăzi! :) Acum este timpul să revenim la rezolvarea sarcinilor!
Comentarii
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION