CodeGym /Java Blog /Willekeurig /is gelijk aan en hashCode-methoden: best practices
John Squirrels
Niveau 41
San Francisco

is gelijk aan en hashCode-methoden: best practices

Gepubliceerd in de groep Willekeurig
Hoi! Vandaag zullen we het hebben over twee belangrijke methoden in Java: equals()en hashCode(). Dit is niet de eerste keer dat we ze ontmoeten: de CodeGym-cursus begint met een korte les over equals()— lees het als je het bent vergeten of nog niet eerder hebt gezien... is gelijk aan en hashCode-methoden: best practices - 1In de les van vandaag zullen we het hebben over deze concepten in detail. En geloof me, we hebben iets om over te praten! Maar laten we, voordat we verder gaan met het nieuwe, opfrissen wat we al behandeld hebben :) Zoals u zich herinnert, is het meestal een slecht idee om twee objecten te vergelijken met behulp van de ==operator, omdat ==vergelijkingen referenties zijn. Hier is ons voorbeeld met auto's uit een recente les:

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

false
Het lijkt erop dat we twee identieke objecten hebben gemaakt Car: de waarden van de overeenkomstige velden van de twee auto-objecten zijn hetzelfde, maar het resultaat van de vergelijking is nog steeds onjuist. We weten de reden al: de verwijzingen car1en car2verwijzen naar verschillende geheugenadressen, dus ze zijn niet gelijk. Maar we willen nog steeds de twee objecten vergelijken, niet twee referenties. De beste oplossing voor het vergelijken van objecten is de equals()methode.

is gelijk aan () methode

U herinnert zich misschien dat we deze methode niet helemaal opnieuw maken, maar dat we deze overschrijven: de equals()methode wordt gedefinieerd in de Objectklasse. Dat gezegd hebbende, in zijn gebruikelijke vorm heeft het weinig zin:

public boolean equals(Object obj) {
   return (this == obj);
}
Dit is hoe de equals()methode in de klasse wordt gedefinieerd Object. Dit is weer een vergelijking van referenties. Waarom hebben ze het zo gemaakt? Welnu, hoe weten de makers van de taal welke objecten in uw programma als gelijk worden beschouwd en welke niet? :) Dit is het belangrijkste punt van de equals()methode — de maker van een klasse is degene die bepaalt welke kenmerken worden gebruikt bij het controleren van de gelijkheid van objecten van de klasse. Dan overschrijf je de equals()methode in je klas. Als u de betekenis van "bepaalt welke kenmerken" niet helemaal begrijpt, laten we eens kijken naar een voorbeeld. Hier is een eenvoudige klasse die een man voorstelt: 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.
}
Stel dat we een programma schrijven dat moet bepalen of twee mensen een eeneiige tweeling zijn of gewoon lookalikes. We hebben vijf kenmerken: neusgrootte, oogkleur, haarstijl, de aanwezigheid van littekens en DNA-testresultaten (voor de eenvoud stellen we dit voor als een geheel getal). Welke van deze kenmerken zou volgens u ons programma in staat stellen om identieke tweelingen te identificeren? is gelijk aan en hashCode-methoden: best practices - 2Uiteraard kan alleen een DNA-test zekerheid bieden. Twee mensen kunnen dezelfde oogkleur, haarsnit, neus en zelfs dezelfde littekens hebben — er zijn veel mensen op de wereld en het is onmogelijk te garanderen dat er geen dubbelgangers zijn. Maar we hebben een betrouwbaar mechanisme nodig: alleen het resultaat van een DNA-test zal ons in staat stellen een juiste conclusie te trekken. Wat betekent dit voor onze equals()werkwijze? We moeten het overschrijven in deManklasse, rekening houdend met de vereisten van ons programma. De methode moet het int dnaCodeveld van de twee objecten vergelijken. Als ze gelijk zijn, dan zijn de objecten gelijk.

@Override
public boolean equals(Object o) {
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
Is het echt zo simpel? Niet echt. We hebben iets over het hoofd gezien. Voor onze objecten hebben we slechts één veld geïdentificeerd dat relevant is voor het vaststellen van objectgelijkheid: dnaCode. Stel je nu voor dat we niet 1, maar 50 relevante velden hebben. En als alle 50 velden van twee objecten gelijk zijn, dan zijn de objecten gelijk. Zo'n scenario is ook mogelijk. Het grootste probleem is dat het vaststellen van gelijkheid door het vergelijken van 50 velden een tijdrovend en arbeidsintensief proces is. ManStel je nu voor dat we naast onze klasse een Womanklasse hebben met exact dezelfde velden die bestaan ​​in Man. Als een andere programmeur onze klassen gebruikt, kan hij of zij gemakkelijk de volgende code schrijven:

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));
}
In dit geval heeft het geen zin om de veldwaarden te controleren: we kunnen gemakkelijk zien dat we objecten van twee verschillende klassen hebben, dus ze kunnen onmogelijk gelijk zijn! Dit betekent dat we een controle aan de equals()methode moeten toevoegen, waarbij de klassen van de vergeleken objecten worden vergeleken. Goed dat we daaraan hebben gedacht!

@Override
public boolean equals(Object o) {
   if (getClass() != o.getClass()) return false;
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
Maar misschien zijn we nog iets vergeten? Hmm... We moeten op zijn minst controleren of we een object niet met zichzelf vergelijken! Als referenties A en B naar hetzelfde geheugenadres wijzen, dan zijn ze hetzelfde object en hoeven we geen tijd te verspillen aan het vergelijken van 50 velden.

@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;
}
Het kan ook geen kwaad om een ​​controle toe te voegen voor null: geen object kan gelijk zijn aan null. Dus als de methodeparameter null is, heeft het geen zin om extra controles uit te voeren. Met dit alles in gedachten, ziet onze equals()methode voor de Manklas er als volgt uit:

@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;
}
Wij voeren alle bovengenoemde eerste controles uit. Aan het eind van de dag, als:
  • we vergelijken twee objecten van dezelfde klasse
  • en de vergeleken objecten zijn niet hetzelfde object
  • en het gepasseerde object nietnull
...dan gaan we over tot een vergelijking van de relevante kenmerken. Voor ons betekent dit de dnaCodevelden van de twee objecten. equals()Houd u bij het overschrijven van de methode aan de volgende vereisten:
  1. Reflexiviteit.

    Wanneer de equals()methode wordt gebruikt om een ​​object met zichzelf te vergelijken, moet deze true retourneren.
    Aan deze eis hebben wij inmiddels voldaan. Onze werkwijze omvat:

    
    if (this == o) return true;
    

  2. Symmetrie.

    Als a.equals(b) == true, dan b.equals(a)moet terugkeren true.
    Ook onze werkwijze voldoet aan deze eis.

  3. Transitiviteit.

    Als twee objecten gelijk zijn aan een derde object, dan moeten ze aan elkaar gelijk zijn.
    Als a.equals(b) == trueen a.equals(c) == true, dan b.equals(c)moet ook waar worden geretourneerd.

  4. Vasthoudendheid.

    Het resultaat van equals()moet alleen veranderen als de betrokken velden worden gewijzigd. Als de gegevens van de twee objecten niet veranderen, equals()moet het resultaat van altijd hetzelfde zijn.

  5. Ongelijkheid met null.

    Voor elk object a.equals(null)moet false worden geretourneerd.
    Dit is niet alleen een reeks "nuttige aanbevelingen", maar eerder een strikt contract , uiteengezet in de Oracle-documentatie

hashCode()-methode

Laten we het nu hebben over de hashCode()methode. Waarom is het nodig? Met precies hetzelfde doel: objecten vergelijken. Maar dat hebben we al equals()! Waarom een ​​andere methode? Het antwoord is simpel: om de prestaties te verbeteren. Een hash-functie, weergegeven in Java met behulp van de hashCode()methode, retourneert een numerieke waarde met een vaste lengte voor elk object. In Java hashCode()retourneert de methode een 32-bits getal ( int) voor elk object. Het vergelijken van twee getallen gaat veel sneller dan het vergelijken van twee objecten met behulp van de equals()methode, vooral als die methode rekening houdt met veel velden. Als ons programma objecten vergelijkt, is dat veel eenvoudiger met een hashcode. Alleen als de objecten op basis van de hashCode()methode gelijk zijn, gaat de vergelijking door naar deequals()methode. Dit is trouwens hoe hash-gebaseerde datastructuren werken, bijvoorbeeld de bekende HashMap! De hashCode()methode equals()wordt, net als de methode, overschreven door de ontwikkelaar. En net als equals(), heeft de hashCode()methode officiële vereisten die worden beschreven in de Oracle-documentatie:
  1. Als twee objecten gelijk zijn (dwz de equals()methode retourneert waar), dan moeten ze dezelfde hashcode hebben.

    Anders zouden onze methoden zinloos zijn. Zoals we hierboven vermeldden, hashCode()moet er eerst een controle plaatsvinden om de prestaties te verbeteren. Als de hash-codes verschillend waren, zou de controle false retourneren, ook al zijn de objecten eigenlijk gelijk volgens hoe we de equals()methode hebben gedefinieerd.

  2. Als de hashCode()methode meerdere keren op hetzelfde object wordt aangeroepen, moet deze elke keer hetzelfde getal teruggeven.

  3. Regel 1 werkt niet averechts. Twee verschillende objecten kunnen dezelfde hashcode hebben.

De derde regel is een beetje verwarrend. Hoe kan dit? De verklaring is vrij simpel. De hashCode()methode retourneert een int. An intis een 32-bits getal. Het heeft een beperkt bereik van waarden: van -2.147.483.648 tot +2.147.483.647. Met andere woorden, er zijn iets meer dan 4 miljard mogelijke waarden voor een int. Stel je nu voor dat je een programma maakt om gegevens op te slaan over alle mensen die op aarde leven. Elke persoon komt overeen met zijn eigen Personobject (vergelijkbaar met de Manklasse). Er leven zo'n 7,5 miljard mensen op de planeet. Met andere woorden, hoe slim het algoritme ook is dat we schrijven voor conversiePersonbezwaar maakt tegen een int, hebben we simpelweg niet genoeg mogelijke getallen. We hebben slechts 4,5 miljard mogelijke int-waarden, maar er zijn veel meer mensen dan dat. Dit betekent dat, hoe hard we ook proberen, sommige mensen dezelfde hashcodes zullen hebben. Wanneer dit gebeurt (hashcodes vallen samen voor twee verschillende objecten) noemen we dit een botsing. Bij het negeren van de hashCode()methode is een van de doelstellingen van de programmeur het minimaliseren van het potentiële aantal botsingen. Rekening houdend met al deze regels, hoe zal de methode er in de klas hashCode()uitzien ? PersonSoortgelijk:

@Override
public int hashCode() {
   return dnaCode;
}
Verrast? :) Als je naar de eisen kijkt, zul je zien dat we ze allemaal naleven. Objecten waarvoor onze equals()methode true retourneert, zullen ook gelijk zijn volgens hashCode(). Als onze twee Personobjecten gelijk zijn in equals(dat wil zeggen, ze hebben dezelfde dnaCode), dan retourneert onze methode hetzelfde getal. Laten we een moeilijker voorbeeld bekijken. Stel dat ons programma luxe auto's voor autoverzamelaars zou moeten selecteren. Verzamelen kan een complexe hobby zijn met veel eigenaardigheden. Een bepaalde auto uit 1963 kan 100 keer meer kosten dan een auto uit 1964. Een rode auto uit 1970 kan 100 keer meer kosten dan een blauwe auto van hetzelfde merk uit hetzelfde jaar. is gelijk aan en hashCode-methoden: best practices - 4In ons vorige voorbeeld, met de Personklasse, hebben we de meeste velden (d.w.z. menselijke kenmerken) weggegooid als onbeduidend en hebben we alleen dednaCodeveld in vergelijkingen. We werken nu in een heel eigenzinnig rijk, waarin geen onbelangrijke details zijn! Hier is onze LuxuryAutoklas:

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 moeten we alle velden in onze vergelijkingen beschouwen. Elke fout kan een klant honderdduizenden dollars kosten, dus het is beter om overdreven veilig te zijn:

@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);
}
In onze equals()methode zijn we alle controles waar we het eerder over hadden niet vergeten. Maar nu vergelijken we elk van de drie velden van onze objecten. Voor dit programma hebben we absolute gelijkheid nodig, dwz gelijkheid van elk veld. Hoe zit het met hashCode?

@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = result + manufactureYear;
   result = result + dollarPrice;
   return result;
}
Het modelveld in onze klasse is een string. Dit is handig, omdat de Stringklasse de methode al overschrijft hashCode(). We berekenen de modelhashcode van het veld en tellen vervolgens de som van de andere twee numerieke velden op. Java-ontwikkelaars hebben een simpele truc die ze gebruiken om het aantal botsingen te verminderen: vermenigvuldig bij het berekenen van een hashcode het tussenresultaat met een oneven priemgetal. Het meest gebruikte getal is 29 of 31. We zullen nu niet ingaan op de wiskundige subtiliteiten, maar onthoud in de toekomst dat het vermenigvuldigen van tussenresultaten met een voldoende groot oneven getal helpt om de resultaten van de hashfunctie te "spreiden" en, verminder daarom het aantal objecten met dezelfde hashcode. Voor onze hashCode()methode in LuxuryAuto zou het er zo uitzien:

@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = 31 * result + manufactureYear;
   result = 31 * result + dollarPrice;
   return result;
}
Je kunt meer lezen over alle fijne kneepjes van dit mechanisme in dit bericht op StackOverflow , evenals in het boek Effective Java van Joshua Bloch. Tot slot nog een belangrijk punt dat het vermelden waard is. equals()Elke keer dat we de methode and overschreven hashCode(), selecteerden we bepaalde instantievelden waarmee in deze methoden rekening wordt gehouden. Deze methoden houden rekening met dezelfde velden. Maar kunnen we verschillende gebieden in equals()en beschouwen hashCode()? Technisch gezien kunnen we dat. Maar dit is een slecht idee, en hier is waarom:

@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;
}
Hier zijn onze equals()en hashCode()methoden voor de LuxuryAutoklas. De hashCode()methode bleef ongewijzigd, maar we hebben het modelveld uit de equals()methode verwijderd. Het model is niet langer een kenmerk dat wordt gebruikt wanneer de equals()methode twee objecten vergelijkt. Maar bij het berekenen van de hashcode wordt toch rekening gehouden met dat veld. Wat krijgen we als resultaat? Laten we twee auto's maken en erachter komen!

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
Fout! Door verschillende velden te gebruiken voor de methoden equals()en hashCode()hebben we de contracten geschonden die ervoor zijn opgesteld! Twee objecten die volgens de equals()methode gelijk zijn, moeten dezelfde hashcode hebben. We hebben er verschillende waarden voor gekregen. Dergelijke fouten kunnen tot absoluut ongelooflijke gevolgen leiden, vooral bij het werken met collecties die een hash gebruiken. Daarom moet u, wanneer u equals()en overschrijft hashCode(), rekening houden met dezelfde velden. Deze les was vrij lang, maar je hebt veel geleerd vandaag! :) Nu is het tijd om terug te gaan naar het oplossen van taken!
Opmerkingen
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION