CodeGym /Java blog /Tilfældig /equals og hashCode metoder: bedste praksis
John Squirrels
Niveau
San Francisco

equals og hashCode metoder: bedste praksis

Udgivet i gruppen
Hej! I dag vil vi tale om to vigtige metoder i Java: equals()og hashCode(). Det er ikke første gang, vi har mødt dem: CodeGym-kurset begynder med en kort lektion om equals()— læs det, hvis du har glemt det eller ikke har set det før... equals og hashCode-metoder: bedste praksis - 1I dagens lektion vil vi tale om disse begreber i detaljer. Og tro mig, vi har noget at tale om! Men før vi går videre til det nye, lad os genopfriske, hvad vi allerede har dækket :) Som du husker, er det normalt en dårlig idé at sammenligne to objekter ved hjælp af operatoren, ==fordi ==sammenligner referencer. Her er vores eksempel med biler fra en nylig lektion:

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);
   }
}
Konsoludgang:

false
Det ser ud til, at vi har skabt to identiske Carobjekter: værdierne af de tilsvarende felter for de to bilobjekter er de samme, men resultatet af sammenligningen er stadig falsk. Vi kender allerede årsagen: referencerne car1og car2peger på forskellige hukommelsesadresser, så de er ikke ens. Men vi ønsker stadig at sammenligne de to objekter, ikke to referencer. Den bedste løsning til at sammenligne objekter er equals()metoden.

equals() metode

Du kan huske, at vi ikke opretter denne metode fra bunden, snarere tilsidesætter vi den: metoden equals()er defineret i Objectklassen. Når det er sagt, er det i sin sædvanlige form til lidt nytte:

public boolean equals(Object obj) {
   return (this == obj);
}
Sådan er equals()metoden defineret i Objectklassen. Dette er endnu en gang en sammenligning af referencer. Hvorfor gjorde de det sådan? Nå, hvordan ved sprogets skabere, hvilke objekter i dit program, der betragtes som ligeværdige, og hvilke der ikke er? :) Dette er hovedpointen i equals()metoden — skaberen af ​​en klasse er den, der bestemmer, hvilke egenskaber der bruges, når man kontrollerer ligheden af ​​objekter i klassen. Så tilsidesætter du equals()metoden i din klasse. Hvis du ikke helt forstår betydningen af ​​"bestemmer hvilke egenskaber", lad os overveje et eksempel. Her er en simpel klasse, der repræsenterer en mand: 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.
}
Antag, at vi skriver et program, der skal afgøre, om to personer er enæggede tvillinger eller blot lookalikes. Vi har fem egenskaber: næsestørrelse, øjenfarve, hårstil, tilstedeværelsen af ​​ar og DNA-testresultater (for nemheds skyld repræsenterer vi dette som en heltalskode). Hvilke af disse egenskaber tror du ville gøre det muligt for vores program at identificere enæggede tvillinger? equals og hashCode-metoder: bedste praksis - 2Det er naturligvis kun en DNA-test, der kan give garanti. To personer kan have den samme øjenfarve, frisure, næse og endda ar - der er mange mennesker i verden, og det er umuligt at garantere, at der ikke er nogen dobbeltgængere derude. Men vi har brug for en pålidelig mekanisme: kun resultatet af en DNA-test vil lade os komme med en nøjagtig konklusion. Hvad betyder det for vores equals()metode? Vi er nødt til at tilsidesætte det iManklasse under hensyntagen til vores programs krav. Metoden skal sammenligne int dnaCodefeltet af de to objekter. Hvis de er ens, så er objekterne ens.

@Override
public boolean equals(Object o) {
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
Er det virkelig så simpelt? Ikke rigtig. Vi overså noget. For vores objekter identificerede vi kun ét felt, der er relevant for at etablere objektlighed: dnaCode. Forestil dig nu, at vi ikke har 1, men 50 relevante felter. Og hvis alle 50 felter af to objekter er ens, så er objekterne ens. Et sådant scenarie er også muligt. Hovedproblemet er, at etablering af ligestilling ved at sammenligne 50 felter er en tidskrævende og ressourcekrævende proces. Forestil dig nu, at vi udover vores Manklasse har en Womanklasse med nøjagtig de samme felter, som findes i Man. Hvis en anden programmør bruger vores klasser, kunne han eller hun nemt skrive kode som dette:

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));
}
I dette tilfælde er det meningsløst at kontrollere feltværdierne: vi kan let se, at vi har objekter af to forskellige klasser, så der er ingen måde, de kan være ens! Det betyder, at vi skal tilføje en check til equals()metoden, der sammenligner klasserne af de sammenlignede objekter. Det er godt, at vi tænkte på det!

@Override
public boolean equals(Object o) {
   if (getClass() != o.getClass()) return false;
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
Men måske har vi glemt noget andet? Hmm... Vi bør som minimum kontrollere, at vi ikke sammenligner et objekt med sig selv! Hvis referencerne A og B peger på den samme hukommelsesadresse, så er de det samme objekt, og vi behøver ikke spilde tid og sammenligne 50 felter.

@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;
}
Det skader heller ikke at tilføje en check for null: intet objekt kan være lig med null. Så hvis metodeparameteren er null, er der ingen mening med yderligere kontrol. Med alt dette i tankerne, så ser vores equals()metode til Manklassen sådan ud:

@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;
}
Vi udfører alle de indledende kontroller nævnt ovenfor. I slutningen af ​​dagen, hvis:
  • vi sammenligner to objekter af samme klasse
  • og de sammenlignede objekter er ikke det samme objekt
  • og det passerede objekt er det ikkenull
...så går vi videre til en sammenligning af de relevante karakteristika. For os betyder det dnaCodefelterne for de to objekter. Når du tilsidesætter equals()metoden, skal du sørge for at overholde disse krav:
  1. Refleksivitet.

    Når equals()metoden bruges til at sammenligne et objekt med sig selv, skal den returnere sand.
    Vi har allerede opfyldt dette krav. Vores metode inkluderer:

    
    if (this == o) return true;
    

  2. Symmetri.

    Hvis a.equals(b) == true, så b.equals(a)skal vende tilbage true.
    Vores metode opfylder også dette krav.

  3. Transitivitet.

    Hvis to objekter er lig med et tredje objekt, så skal de være lig med hinanden.
    Hvis a.equals(b) == trueog a.equals(c) == true, b.equals(c)skal så også returnere sandt.

  4. Udholdenhed.

    Resultatet af equals()må kun ændres, når de involverede felter ændres. Hvis dataene for de to objekter ikke ændres, så equals()skal resultatet af altid være det samme.

  5. Ulighed med null.

    For ethvert objekt, a.equals(null)skal returnere falsk
    Dette er ikke bare et sæt af nogle "nyttige anbefalinger", men snarere en streng kontrakt , der er beskrevet i Oracle-dokumentationen

hashCode() metode

Lad os nu tale om hashCode()metoden. Hvorfor er det nødvendigt? Til nøjagtig samme formål - at sammenligne objekter. Men det har vi allerede equals()! Hvorfor en anden metode? Svaret er enkelt: at forbedre ydeevnen. En hash-funktion, repræsenteret i Java ved hjælp af metoden hashCode(), returnerer en numerisk værdi med fast længde for ethvert objekt. I Java hashCode()returnerer metoden et 32-bit tal ( int) for ethvert objekt. At sammenligne to tal er meget hurtigere end at sammenligne to objekter ved hjælp af equals()metoden, især hvis den metode tager mange felter i betragtning. Hvis vores program sammenligner objekter, er dette meget nemmere at gøre ved at bruge en hash-kode. Kun hvis objekterne er ens baseret på hashCode()metoden, fortsætter sammenligningen tilequals()metode. Det er i øvrigt sådan hash-baserede datastrukturer fungerer, for eksempel det velkendte HashMap! Metoden er hashCode()ligesom equals()metoden tilsidesat af udvikleren. Og ligesom equals(), hashCode()har metoden officielle krav beskrevet i Oracle-dokumentationen:
  1. Hvis to objekter er ens (dvs. equals()metoden returnerer sand), så skal de have samme hash-kode.

    Ellers ville vores metoder være meningsløse. Som vi nævnte ovenfor, hashCode()bør en check gå først for at forbedre ydeevnen. Hvis hash-koderne var forskellige, ville checken returnere falsk, selvom objekterne faktisk er ens i henhold til, hvordan vi har defineret metoden equals().

  2. Hvis hashCode()metoden kaldes flere gange på samme objekt, skal den returnere det samme tal hver gang.

  3. Regel 1 virker ikke i den modsatte retning. To forskellige objekter kan have den samme hash-kode.

Den tredje regel er lidt forvirrende. Hvordan kan det være? Forklaringen er ret enkel. Metoden hashCode()returnerer en int. An inter et 32-bit tal. Den har et begrænset værdiområde: fra -2.147.483.648 til +2.147.483.647. Med andre ord er der godt 4 milliarder mulige værdier for en int. Forestil dig nu, at du laver et program til at gemme data om alle mennesker, der lever på Jorden. Hver person vil svare til sit eget Personobjekt (svarende til Manklassen). Der bor ~7,5 milliarder mennesker på planeten. Med andre ord, uanset hvor smart algoritmen vi skriver til konverteringPersonobjekter til en int, har vi simpelthen ikke nok mulige tal. Vi har kun 4,5 milliarder mulige int-værdier, men der er mange flere mennesker end det. Det betyder, at uanset hvor meget vi prøver, vil nogle forskellige mennesker have de samme hash-koder. Når dette sker (hash-koder falder sammen for to forskellige objekter) kalder vi det en kollision. Når man tilsidesætter hashCode()metoden, er et af programmørens mål at minimere det potentielle antal kollisioner. Når man tager højde for alle disse regler, hvordan vil hashCode()metoden se ud i Personklassen? Sådan her:

@Override
public int hashCode() {
   return dnaCode;
}
Overrasket? :) Hvis du ser på kravene, vil du se, at vi overholder dem alle. Objekter, for hvilke vores equals()metode returnerer sand, vil også være ens i henhold til hashCode(). Hvis vores to Personobjekter er lige store i equals(det vil sige, de har det samme dnaCode), så returnerer vores metode det samme tal. Lad os overveje et mere vanskeligt eksempel. Antag, at vores program skulle vælge luksusbiler til bilsamlere. Indsamling kan være en kompleks hobby med mange ejendommeligheder. En bestemt 1963-bil kan koste 100 gange mere end en 1964-bil. En rød bil fra 1970 kan koste 100 gange mere end en blå bil af samme mærke af samme år. equals og hashCode metoder: bedste praksis - 4I vores tidligere eksempel, med Personklassen, kasserede vi de fleste af felterne (dvs. menneskelige egenskaber) som ubetydelige og brugte kundnaCodefelt i sammenligninger. Vi arbejder nu i et meget idiosynkratisk område, hvor der ikke er nogen ubetydelige detaljer! Her er vores LuxuryAutoklasse:

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.
}
Nu skal vi overveje alle felterne i vores sammenligninger. Enhver fejltagelse kan koste en klient hundredtusindvis af dollars, så det ville være bedre at være alt for sikker:

@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);
}
I vores equals()metode har vi ikke glemt alle de checks, vi talte om tidligere. Men nu sammenligner vi hvert af de tre felter af vores objekter. Til dette program har vi brug for absolut lighed, dvs. lighed på hvert område. Hvad med hashCode?

@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = result + manufactureYear;
   result = result + dollarPrice;
   return result;
}
Feltet modeli vores klasse er en streng. Dette er praktisk, fordi Stringklassen allerede tilsidesætter hashCode()metoden. Vi beregner modelfeltets hash-kode og lægger derefter summen af ​​de to andre numeriske felter til den. Java-udviklere har et simpelt trick, som de bruger til at reducere antallet af kollisioner: Når du beregner en hash-kode, skal du gange mellemresultatet med et ulige primtal. Det mest brugte tal er 29 eller 31. Vi vil ikke dykke ned i de matematiske finesser lige nu, men i fremtiden skal du huske, at multiplikation af mellemresultater med et tilstrækkeligt stort ulige tal hjælper med at "sprede" resultaterne af hashfunktionen og, reducere derfor antallet af objekter med den samme hash-kode. For vores hashCode()metode i LuxuryAuto ville det se sådan ud:

@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = 31 * result + manufactureYear;
   result = 31 * result + dollarPrice;
   return result;
}
Du kan læse mere om alle forviklingerne ved denne mekanisme i dette indlæg på StackOverflow såvel som i bogen Effektiv Java af Joshua Bloch. Til sidst endnu et vigtigt punkt, som er værd at nævne. Hver gang vi tilsidesatte metoden equals()og hashCode(), valgte vi visse instansfelter, der tages i betragtning i disse metoder. Disse metoder overvejer de samme felter. Men kan vi overveje forskellige felter i equals()og hashCode()? Teknisk set kan vi. Men det er en dårlig idé, og her er hvorfor:

@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;
}
Her er vores equals()og hashCode()metoder til LuxuryAutoklassen. Metoden hashCode()forblev uændret, men vi fjernede modelfeltet fra equals()metoden. Modellen er ikke længere en egenskab, der bruges, når equals()metoden sammenligner to objekter. Men når man beregner hash-koden, tages det felt stadig i betragtning. Hvad får vi som resultat? Lad os skabe to biler og finde ud af det!

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
Fejl! Ved at bruge forskellige felter til equals()og hashCode()metoder, overtrådte vi de kontrakter, der er blevet etableret for dem! To objekter, der er ens ifølge equals()metoden, skal have samme hash-kode. Vi fik forskellige værdier for dem. Sådanne fejl kan føre til helt utrolige konsekvenser, især når man arbejder med samlinger, der bruger en hash. Derfor bør du overveje de samme felter, når du tilsidesætter equals()og . hashCode()Denne lektion var ret lang, men du lærte meget i dag! :) Nu er det tid til at komme tilbage til at løse opgaver!
Kommentarer
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION