CodeGym /Java blogg /Slumpmässig /equals och hashCode-metoder: bästa praxis
John Squirrels
Nivå
San Francisco

equals och hashCode-metoder: bästa praxis

Publicerad i gruppen
Hej! Idag ska vi prata om två viktiga metoder i Java: equals()och hashCode(). Det här är inte första gången vi träffar dem: CodeGym-kursen börjar med en kort lektion om equals()— läs den om du har glömt den eller inte sett den förut... equals och hashCode-metoder: bästa praxis - 1I dagens lektion ska vi prata om dessa begrepp i detalj. Och tro mig, vi har något att prata om! Men innan vi går vidare till det nya, låt oss uppdatera det vi redan har täckt :) Som ni minns är det vanligtvis en dålig idé att jämföra två objekt med operatorn, ==eftersom ==jämför referenser. Här är vårt exempel med bilar från en ny 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);
   }
}
Konsolutgång:

false
Det verkar som att vi har skapat två identiska Carobjekt: värdena för motsvarande fält för de två bilobjekten är desamma, men resultatet av jämförelsen är fortfarande falskt. Vi vet redan orsaken: referenserna car1och car2pekar på olika minnesadresser, så de är inte lika. Men vi vill ändå jämföra de två objekten, inte två referenser. Den bästa lösningen för att jämföra objekt är equals()metoden.

metoden equals().

Du kanske minns att vi inte skapar den här metoden från början, utan vi åsidosätter den: metoden equals()definieras i Objectklassen. Som sagt, i sin vanliga form är det till liten nytta:

public boolean equals(Object obj) {
   return (this == obj);
}
Så här equals()definieras metoden i Objectklassen. Detta är återigen en jämförelse av referenser. Varför gjorde de det så? Tja, hur vet språkets skapare vilka objekt i ditt program som anses likvärdiga och vilka som inte är det? :) Detta är huvudpoängen med metoden equals()— skaparen av en klass är den som bestämmer vilka egenskaper som används när man kontrollerar likheten mellan objekt i klassen. Sedan åsidosätter du equals()metoden i din klass. Om du inte riktigt förstår innebörden av "bestämmer vilka egenskaper", låt oss överväga ett exempel. Här är en enkel klass som representerar en man: 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.
}
Anta att vi skriver ett program som måste avgöra om två personer är enäggstvillingar eller helt enkelt lookalikes. Vi har fem egenskaper: nässtorlek, ögonfärg, hårstil, förekomsten av ärr och DNA-testresultat (för enkelhetens skull representerar vi detta som en heltalskod). Vilka av dessa egenskaper tror du skulle göra det möjligt för vårt program att identifiera enäggstvillingar? equals och hashCode-metoder: bästa praxis - 2Självklart kan bara ett DNA-test ge en garanti. Två personer kan ha samma ögonfärg, frisyr, näsa och till och med ärr - det finns många människor i världen, och det är omöjligt att garantera att det inte finns några dubbelgängare där ute. Men vi behöver en pålitlig mekanism: endast resultatet av ett DNA-test låter oss göra en korrekt slutsats. Vad betyder detta för vår equals()metod? Vi måste åsidosätta det iManklass, med hänsyn till vårt programs krav. Metoden ska jämföra int dnaCodefältet för de två objekten. Om de är lika, då är objekten lika.

@Override
public boolean equals(Object o) {
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
Är det verkligen så enkelt? Inte riktigt. Vi förbisåg något. För våra objekt identifierade vi endast ett fält som är relevant för att fastställa objektlikhet: dnaCode. Föreställ dig nu att vi inte har 1, utan 50 relevanta fält. Och om alla 50 fält av två objekt är lika, då är objekten lika. Ett sådant scenario är också möjligt. Det största problemet är att det är en tidskrävande och resurskrävande process att skapa jämställdhet genom att jämföra 50 fält. ManFöreställ dig nu att vi förutom vår klass har en Womanklass med exakt samma fält som finns i Man. Om en annan programmerare använder våra klasser kan han eller hon enkelt skriva kod så här:

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 det här fallet är det meningslöst att kontrollera fältvärdena: vi kan lätt se att vi har objekt av två olika klasser, så det finns inget sätt att de kan vara lika! Detta betyder att vi bör lägga till en kontroll i equals()metoden, jämföra klasserna för de jämförda objekten. Det är bra att 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 vi kanske har glömt något annat? Hmm... Åtminstone bör vi kontrollera att vi inte jämför ett objekt med sig själv! Om referenserna A och B pekar på samma minnesadress, så är de samma objekt, och vi behöver inte slösa tid och jämföra 50 fält.

@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 skadar inte heller att lägga till en check för null: inget objekt kan vara lika med null. Så om metodparametern är null, är det ingen mening med ytterligare kontroller. Med allt detta i åtanke ser vår equals()metod för Manklassen ut så här:

@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ör alla inledande kontroller som nämns ovan. I slutet av dagen, om:
  • vi jämför två objekt av samma klass
  • och de jämförda objekten är inte samma objekt
  • och det passerade objektet är det intenull
...sedan går vi vidare till en jämförelse av de relevanta egenskaperna. För oss betyder det dnaCodefälten för de två objekten. När du åsidosätter equals()metoden, se till att följa dessa krav:
  1. Reflexivitet.

    När equals()metoden används för att jämföra ett objekt med sig självt måste den returnera sant.
    Vi har redan uppfyllt detta krav. Vår metod inkluderar:

    
    if (this == o) return true;
    

  2. Symmetri.

    Om a.equals(b) == true, då b.equals(a)måste återvända true.
    Vår metod uppfyller även detta krav.

  3. Transitivitet.

    Om två objekt är lika med något tredje objekt måste de vara lika med varandra.
    Om a.equals(b) == trueoch a.equals(c) == true, då b.equals(c)måste också returnera sant.

  4. Uthållighet.

    Resultatet av equals()måste ändras först när de berörda fälten ändras. Om data för de två objekten inte ändras, måste resultatet av equals()alltid vara detsamma.

  5. Ojämlikhet med null.

    För alla objekt, a.equals(null)måste returnera false
    Detta är inte bara en uppsättning av några "användbara rekommendationer", utan snarare ett strikt kontrakt , som anges i Oracle-dokumentationen

hashCode() metod

Låt oss nu prata om hashCode()metoden. Varför är det nödvändigt? För exakt samma syfte — att jämföra objekt. Men det har vi redan equals()! Varför en annan metod? Svaret är enkelt: att förbättra prestandan. En hashfunktion, representerad i Java med metoden, hashCode()returnerar ett numeriskt värde med fast längd för alla objekt. I Java hashCode()returnerar metoden ett 32-bitars nummer ( int) för alla objekt. Att jämföra två tal är mycket snabbare än att jämföra två objekt med equals()metoden, särskilt om den metoden tar hänsyn till många fält. Om vårt program jämför objekt är detta mycket enklare att göra med hjälp av en hash-kod. Endast om objekten är lika baserat på hashCode()metoden fortsätter jämförelsen tillequals()metod. Det är förresten så här hashbaserade datastrukturer fungerar, till exempel den välbekanta HashMap! Metoden hashCode(), liksom equals()metoden, åsidosätts av utvecklaren. Och precis som equals(), har metoden hashCode()officiella krav som anges i Oracle-dokumentationen:
  1. Om två objekt är lika (dvs metoden equals()returnerar true), måste de ha samma hash-kod.

    Annars skulle våra metoder vara meningslösa. Som vi nämnde ovan hashCode()bör en check gå först för att förbättra prestandan. Om hashkoderna var olika, skulle kontrollen returnera falskt, även om objekten faktiskt är lika enligt hur vi har definierat metoden equals().

  2. Om hashCode()metoden anropas flera gånger på samma objekt måste den returnera samma nummer varje gång.

  3. Regel 1 fungerar inte i motsatt riktning. Två olika objekt kan ha samma hashkod.

Den tredje regeln är lite förvirrande. Hur kan det vara såhär? Förklaringen är ganska enkel. Metoden hashCode()returnerar en int. An intär ett 32-bitars nummer. Den har ett begränsat värdeintervall: från -2 147 483 648 till +2 147 483 647. Det finns med andra ord drygt 4 miljarder möjliga värden för en int. Föreställ dig nu att du skapar ett program för att lagra data om alla människor som lever på jorden. Varje person kommer att motsvara sitt eget Personobjekt (liknande klassen) Man. Det finns ~7,5 miljarder människor på jorden. Med andra ord, oavsett hur smart algoritmen vi skriver för konverteringPersoninvänder mot en int, vi har helt enkelt inte tillräckligt många möjliga siffror. Vi har bara 4,5 miljarder möjliga int-värden, men det finns mycket fler människor än så. Det betyder att oavsett hur mycket vi försöker, kommer vissa personer att ha samma hashkoder. När detta händer (hash-koder sammanfaller för två olika objekt) kallar vi det en kollision. När man åsidosätter hashCode()metoden är ett av programmerarens mål att minimera det potentiella antalet kollisioner. Redovisa för alla dessa regler, hur kommer hashCode()metoden att se ut i Personklassen? Så här:

@Override
public int hashCode() {
   return dnaCode;
}
Överraskad? :) Om du tittar på kraven ser du att vi uppfyller dem alla. Objekt för vilka vår equals()metod returnerar sant kommer också att vara lika enligt hashCode(). Om våra två Personobjekt är lika i equals(det vill säga de har samma dnaCode), så returnerar vår metod samma antal. Låt oss överväga ett svårare exempel. Anta att vårt program ska välja ut lyxbilar för bilsamlare. Att samla kan vara en komplex hobby med många egenheter. En speciell bil från 1963 kan kosta 100 gånger mer än en bil från 1964. En röd bil från 1970 kan kosta 100 gånger mer än en blå bil av samma märke samma år. equals och hashCode-metoder: bästa praxis - 4I vårt tidigare exempel, med Personklassen, kasserade vi de flesta fälten (dvs. mänskliga egenskaper) som obetydliga och använde endastdnaCodefält i jämförelser. Vi arbetar nu i en väldigt egendomlig värld, där det inte finns några obetydliga detaljer! Här är vår LuxuryAutoklass:

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 måste vi överväga alla områden i våra jämförelser. Alla misstag kan kosta en kund hundratusentals dollar, så det skulle vara bättre att vara alltför säker:

@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()metod har vi inte glömt alla kontroller vi pratat om tidigare. Men nu jämför vi vart och ett av de tre fälten för våra objekt. För detta program behöver vi absolut jämlikhet, dvs. jämlikhet inom varje område. Vad sägs om hashCode?

@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = result + manufactureYear;
   result = result + dollarPrice;
   return result;
}
Fältet modeli vår klass är en sträng. Detta är bekvämt eftersom Stringklassen redan åsidosätter hashCode()metoden. Vi beräknar modelfältets hashkod och lägger sedan till summan av de andra två numeriska fälten till den. Java-utvecklare har ett enkelt knep som de använder för att minska antalet kollisioner: när du beräknar en hashkod, multiplicera mellanresultatet med ett udda primtal. Det vanligaste talet är 29 eller 31. Vi kommer inte att fördjupa oss i de matematiska subtiliteterna just nu, men i framtiden kom ihåg att multiplicering av mellanresultat med ett tillräckligt stort udda tal hjälper till att "sprida ut" resultaten av hashfunktionen och, minska därför antalet objekt med samma hashkod. För vår hashCode()metod i LuxuryAuto skulle det se ut så här:

@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = 31 * result + manufactureYear;
   result = 31 * result + dollarPrice;
   return result;
}
Du kan läsa mer om alla krångligheterna med denna mekanism i det här inlägget på StackOverflow , såväl som i boken Effektiv Java av Joshua Bloch. Slutligen, ytterligare en viktig punkt som är värd att nämna. Varje gång vi åsidosatte equals()metoden hashCode()och valde vi vissa instansfält som tas med i beräkningen i dessa metoder. Dessa metoder beaktar samma områden. Men kan vi överväga olika områden inom equals()och hashCode()? Tekniskt sett kan vi det. Men det här är en dålig idé, och här är anledningen:

@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;
}
Här är våra equals()och hashCode()metoder för LuxuryAutoklassen. Metoden hashCode()förblev oförändrad, men vi tog bort modelfältet från equals()metoden. Modellen är inte längre en egenskap som används när equals()metoden jämför två objekt. Men när man beräknar hashkoden tas det fältet fortfarande med i beräkningen. Vad får vi som resultat? Låt oss skapa två bilar och ta reda på 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
Fel! Genom att använda olika fält för equals()och hashCode()metoder har vi brutit mot de avtal som har upprättats för dem! Två objekt som är lika enligt equals()metoden måste ha samma hashkod. Vi fick olika värderingar för dem. Sådana fel kan leda till helt otroliga konsekvenser, speciellt när man arbetar med samlingar som använder en hash. Som ett resultat, när du åsidosätter equals()och hashCode()bör du överväga samma fält. Den här lektionen var ganska lång, men du lärde dig mycket idag! :) Nu är det dags att återgå till att lösa uppgifter!
Kommentarer
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION