CodeGym /Java-blogg /Tilfeldig /equals og hashCode-metoder: beste praksis
John Squirrels
Nivå
San Francisco

equals og hashCode-metoder: beste praksis

Publisert i gruppen
Hei! I dag skal vi snakke om to viktige metoder i Java: equals()og hashCode(). Dette er ikke første gang vi møter dem: CodeGym-kurset begynner med en kort leksjon om equals()— les den hvis du har glemt den eller ikke har sett den før... equals og hashCode-metoder: beste praksis - 1I dagens leksjon skal vi snakke om disse konseptene i detalj. Og tro meg, vi har noe å snakke om! Men før vi går videre til det nye, la oss oppdatere det vi allerede har dekket :) Som du husker, er det vanligvis en dårlig idé å sammenligne to objekter ved å bruke operatoren, ==fordi ==sammenligner referanser. Her er vårt eksempel med biler fra en nylig leksjon:

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

false
Det ser ut til at vi har laget to identiske Carobjekter: verdiene til de tilsvarende feltene til de to bilobjektene er de samme, men resultatet av sammenligningen er fortsatt usant. Vi vet allerede årsaken: referansene car1og car2peker til forskjellige minneadresser, så de er ikke like. Men vi ønsker likevel å sammenligne de to objektene, ikke to referanser. Den beste løsningen for å sammenligne objekter er equals()metoden.

equals() metode

Du husker kanskje at vi ikke oppretter denne metoden fra bunnen av, snarere overstyrer vi den: metoden equals()er definert i Objectklassen. Når det er sagt, i sin vanlige form, er det til liten nytte:

public boolean equals(Object obj) {
   return (this == obj);
}
Slik er equals()metoden definert i Objectklassen. Dette er nok en gang en sammenligning av referanser. Hvorfor gjorde de det slik? Vel, hvordan vet språkets skapere hvilke objekter i programmet ditt som anses som likeverdige og hvilke som ikke er det? :) Dette er hovedpoenget med equals()metoden — skaperen av en klasse er den som bestemmer hvilke egenskaper som brukes når man sjekker likheten mellom objekter i klassen. Deretter overstyrer du equals()metoden i klassen din. Hvis du ikke helt forstår betydningen av "avgjør hvilke egenskaper", la oss vurdere et eksempel. Her er en enkel klasse som representerer en mann: 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.
}
Tenk deg at vi skriver et program som må finne ut om to personer er identiske tvillinger eller bare lookalikes. Vi har fem egenskaper: nesestørrelse, øyenfarge, hårstil, tilstedeværelse av arr og DNA-testresultater (for enkelhets skyld representerer vi dette som en heltallskode). Hvilke av disse egenskapene tror du vil tillate programmet vårt å identifisere identiske tvillinger? equals og hashCode-metoder: beste praksis - 2Det er selvsagt kun en DNA-test som kan gi en garanti. To personer kan ha samme øyenfarge, hårklipp, nese og til og med arr - det er mange mennesker i verden, og det er umulig å garantere at det ikke er noen doppelgjengere der ute. Men vi trenger en pålitelig mekanisme: bare resultatet av en DNA-test lar oss gjøre en nøyaktig konklusjon. Hva betyr dette for equals()metoden vår? Vi må overstyre det iManklasse, tar hensyn til programmets krav. Metoden skal sammenligne int dnaCodefeltet til de to objektene. Hvis de er like, så er objektene like.

@Override
public boolean equals(Object o) {
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
Er det virkelig så enkelt? Ikke egentlig. Vi overså noe. For våre objekter identifiserte vi kun ett felt som er relevant for å etablere objektlikhet: dnaCode. Tenk deg nå at vi ikke har 1, men 50 relevante felt. Og hvis alle de 50 feltene til to objekter er like, så er objektene like. Et slikt scenario er også mulig. Hovedproblemet er at å etablere likestilling ved å sammenligne 50 felt er en tidkrevende og ressurskrevende prosess. Tenk deg nå at vi i tillegg til Manklassen vår har en Womanklasse med nøyaktig de samme feltene som finnes i Man. Hvis en annen programmerer bruker klassene våre, kan han eller hun enkelt skrive kode slik:

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 tilfellet er det meningsløst å sjekke feltverdiene: vi kan lett se at vi har objekter av to forskjellige klasser, så det er ingen måte de kan være like! Dette betyr at vi bør legge til en sjekk til equals()metoden, sammenligne klassene til de sammenlignede objektene. Det er bra at vi har tenkt på det!

@Override
public boolean equals(Object o) {
   if (getClass() != o.getClass()) return false;
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
Men kanskje vi har glemt noe annet? Hmm... Vi bør i det minste sjekke at vi ikke sammenligner et objekt med seg selv! Hvis referansene A og B peker på samme minneadresse, er de samme objektet, og vi trenger ikke kaste bort tid og sammenligne 50 felt.

@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 å legge til en sjekk for null: ingen objekter kan være lik null. Så hvis metodeparameteren er null, er det ingen vits i ytterligere kontroller. Med alt dette i tankene, så ser equals()metoden vår for Manklassen slik ut:

@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 utfører alle de innledende kontrollene nevnt ovenfor. På slutten av dagen, hvis:
  • vi sammenligner to objekter av samme klasse
  • og de sammenlignede objektene er ikke det samme objektet
  • og det passerte objektet er det ikkenull
...så går vi videre til en sammenligning av de relevante egenskapene. For oss betyr dette feltene dnaCodetil de to objektene. Når du overstyrer equals()metoden, sørg for å overholde disse kravene:
  1. Refleksivitet.

    Når equals()metoden brukes til å sammenligne et objekt med seg selv, må den returnere sant.
    Vi har allerede overholdt dette kravet. Vår metode inkluderer:

    
    if (this == o) return true;
    

  2. Symmetri.

    Hvis a.equals(b) == true, så b.equals(a)må returnere true.
    Metoden vår tilfredsstiller også dette kravet.

  3. Transitivitet.

    Hvis to objekter er like med et tredje objekt, må de være like med hverandre.
    Hvis a.equals(b) == trueog a.equals(c) == true, b.equals(c)må da også returnere sant.

  4. Standhaftighet.

    Resultatet av equals()må bare endres når de involverte feltene endres. Hvis dataene til de to objektene ikke endres, må resultatet av equals()alltid være det samme.

  5. Ulikhet med null.

    For ethvert objekt, a.equals(null)må returnere false
    Dette er ikke bare et sett med noen "nyttige anbefalinger", men snarere en streng kontrakt , angitt i Oracle-dokumentasjonen

hashCode()-metoden

La oss nå snakke om hashCode()metoden. Hvorfor er det nødvendig? For nøyaktig samme formål - å sammenligne objekter. Men det har vi allerede equals()! Hvorfor en annen metode? Svaret er enkelt: å forbedre ytelsen. En hash-funksjon, representert i Java ved bruk av hashCode()metoden, returnerer en numerisk verdi med fast lengde for ethvert objekt. I Java hashCode()returnerer metoden et 32-biters tall ( int) for ethvert objekt. Å sammenligne to tall er mye raskere enn å sammenligne to objekter ved hjelp av equals()metoden, spesielt hvis den metoden vurderer mange felt. Hvis programmet vårt sammenligner objekter, er dette mye enklere å gjøre ved å bruke en hash-kode. Bare hvis objektene er like basert på hashCode()metoden, fortsetter sammenligningen tilequals()metode. Det er forresten slik hasjbaserte datastrukturer fungerer, for eksempel det kjente HashMap! Metoden hashCode(), i likhet med equals()metoden, overstyres av utvikleren. Og akkurat som equals(), hashCode()har metoden offisielle krav spesifisert i Oracle-dokumentasjonen:
  1. Hvis to objekter er like (dvs. equals()metoden returnerer true), må de ha samme hash-kode.

    Ellers ville våre metoder vært meningsløse. Som vi nevnte ovenfor, hashCode()bør en sjekk gå først for å forbedre ytelsen. Hvis hash-kodene var forskjellige, ville sjekken returnert falsk, selv om objektene faktisk er like i henhold til hvordan vi har definert metoden equals().

  2. Hvis hashCode()metoden kalles flere ganger på samme objekt, må den returnere samme nummer hver gang.

  3. Regel 1 fungerer ikke i motsatt retning. To forskjellige objekter kan ha samme hash-kode.

Den tredje regelen er litt forvirrende. Hvordan kan dette være? Forklaringen er ganske enkel. Metoden hashCode()returnerer en int. An inter et 32-bits tall. Den har et begrenset verdiområde: fra -2.147.483.648 til +2.147.483.647. Det er med andre ord i overkant av 4 milliarder mulige verdier for en int. Tenk deg nå at du lager et program for å lagre data om alle mennesker som bor på jorden. Hver person vil korrespondere med sitt eget Personobjekt (i likhet med Manklassen). Det bor ~7,5 milliarder mennesker på planeten. Med andre ord, uansett hvor smart algoritmen vi skriver for konverteringPersonobjekter til en int, har vi rett og slett ikke nok mulige tall. Vi har bare 4,5 milliarder mulige int-verdier, men det er mye flere mennesker enn det. Dette betyr at uansett hvor hardt vi prøver, vil noen forskjellige personer ha de samme hashkodene. Når dette skjer (hash-koder sammenfaller for to forskjellige objekter) kaller vi det en kollisjon. Når man overstyrer hashCode()metoden, er et av programmererens mål å minimere det potensielle antallet kollisjoner. Regnskap for alle disse reglene, hvordan vil hashCode()metoden se ut i Personklassen? Som dette:

@Override
public int hashCode() {
   return dnaCode;
}
Overrasket? :) Hvis du ser på kravene, vil du se at vi overholder dem alle. Objekter som equals()metoden vår returnerer sann for vil også være like i henhold til hashCode(). Hvis våre to Personobjekter er like i equals(det vil si at de har samme dnaCode), returnerer metoden vår samme tall. La oss vurdere et vanskeligere eksempel. Anta at programmet vårt skal velge luksusbiler for bilsamlere. Samling kan være en kompleks hobby med mange særegenheter. En spesiell 1963-bil kan koste 100 ganger mer enn en 1964-bil. En rød bil fra 1970 kan koste 100 ganger mer enn en blå bil av samme merke samme år. equals og hashCode-metoder: beste praksis - 4I vårt forrige eksempel, med klassen Person, forkastet vi de fleste feltene (dvs. menneskelige egenskaper) som ubetydelige og brukte barednaCodefelt i sammenligninger. Vi jobber nå i et veldig idiosynkratisk rike, der det ikke er noen ubetydelige detaljer! Her er LuxuryAutoklassen vår:

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.
}
Nå må vi vurdere alle feltene i våre sammenligninger. Enhver feil kan koste en klient hundretusenvis av dollar, så det ville være bedre å være for trygg:

@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 vår equals()metode har vi ikke glemt alle sjekkene vi snakket om tidligere. Men nå sammenligner vi hvert av de tre feltene til objektene våre. For dette programmet trenger vi absolutt likhet, dvs. likhet på hvert felt. Hva med hashCode?

@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = result + manufactureYear;
   result = result + dollarPrice;
   return result;
}
Feltet modeli klassen vår er en streng. Dette er praktisk, fordi Stringklassen allerede overstyrer hashCode()metoden. Vi beregner modelfeltets hash-kode og legger deretter summen av de to andre numeriske feltene til den. Java-utviklere har et enkelt triks som de bruker for å redusere antall kollisjoner: når du beregner en hash-kode, multipliser mellomresultatet med en oddetall. Det mest brukte tallet er 29 eller 31. Vi skal ikke fordype oss i de matematiske subtilitetene akkurat nå, men i fremtiden husk at å multiplisere mellomresultater med et tilstrekkelig stort oddetall hjelper til med å "spre ut" resultatene av hashfunksjonen og, Reduser derfor antallet objekter med samme hash-kode. For vår hashCode()metode i LuxuryAuto vil den se slik ut:

@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = 31 * result + manufactureYear;
   result = 31 * result + dollarPrice;
   return result;
}
Du kan lese mer om alle forviklingene ved denne mekanismen i dette innlegget på StackOverflow , samt i boken Effektiv Java av Joshua Bloch. Til slutt et viktig poeng som er verdt å nevne. Hver gang vi overstyrte metoden equals()og hashCode(), valgte vi visse forekomstfelt som er tatt i betraktning i disse metodene. Disse metodene vurderer de samme feltene. Men kan vi vurdere ulike felt i equals()og hashCode()? Teknisk sett kan vi det. Men dette er en dårlig idé, og her er grunnen:

@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 våre equals()og hashCode()metoder for LuxuryAutoklassen. Metoden hashCode()forble uendret, men vi fjernet modelfeltet fra equals()metoden. Modellen er ikke lenger en egenskap som brukes når equals()metoden sammenligner to objekter. Men når man beregner hash-koden, tas det fortsatt hensyn til det feltet. Hva får vi som resultat? La oss lage to biler og finne ut!

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
Feil! Ved å bruke ulike felt for equals()og hashCode()metoder, brøt vi kontraktene som er etablert for dem! To objekter som er like i henhold til equals()metoden må ha samme hash-kode. Vi fikk forskjellige verdier for dem. Slike feil kan føre til helt utrolige konsekvenser, spesielt når man jobber med samlinger som bruker en hash. Som et resultat, når du overstyrer equals()og hashCode(), bør du vurdere de samme feltene. Denne leksjonen var ganske lang, men du lærte mye i dag! :) Nå er det på tide å komme tilbake til å løse oppgaver!
Kommentarer
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION