CodeGym /Java blog /Véletlen /egyenlő és hashCode metódusok: legjobb gyakorlatok
John Squirrels
Szint
San Francisco

egyenlő és hashCode metódusok: legjobb gyakorlatok

Megjelent a csoportban
Szia! Ma két fontos Java módszerről fogunk beszélni: equals()és hashCode(). Nem először találkozunk velük: a CodeGym tanfolyam egy rövid leckével kezdődik equals(): olvasd el, ha elfelejtetted, vagy még nem láttad... egyenlő és hashCode metódusok: legjobb gyakorlatok - 1A mai leckében arról lesz szó, ezeket a fogalmakat részletesen. És hidd el, van miről beszélnünk! De mielőtt rátérnénk az újra, frissítsük fel a már leírtakat :) Mint emlékszel, általában rossz ötlet két objektumot összehasonlítani az operátorral ==, mert ==összehasonlítja a hivatkozásokat. Íme a példánk az autókkal egy közelmúltbeli leckéből:

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

false
Úgy tűnik, két azonos objektumot hoztunk létre Car: a két autóobjektum megfelelő mezőinek értékei megegyeznek, de az összehasonlítás eredménye így is hamis. Az okot már tudjuk: a car1és car2hivatkozások különböző memóriacímekre mutatnak, tehát nem egyenlőek. De továbbra is a két objektumot szeretnénk összehasonlítani, nem két hivatkozást. Az objektumok összehasonlítására a legjobb megoldás a equals()módszer.

egyenlő() metódus

Emlékezhet, hogy ezt a metódust nem a semmiből hozzuk létre, hanem felülírjuk: a equals()metódus az osztályban van definiálva Object. Ez azt jelenti, hogy a szokásos formájában nem sok haszna van:

public boolean equals(Object obj) {
   return (this == obj);
}
Így van equals()definiálva a metódus az osztályban Object. Ez ismét a hivatkozások összehasonlítása. Miért csinálták így? Nos, honnan tudják a nyelv készítői, hogy a programodban mely objektumok számítanak egyenlőnek és melyek nem? :) Ez a metódus lényege equals()– az osztály létrehozója az, aki meghatározza, hogy mely jellemzőket használjuk az osztály objektumai egyenlőségének ellenőrzésekor. Ezután felülírja a equals()módszert az osztályában. Ha nem egészen érti a „meghatározza, mely jellemzőket” jelentését, nézzünk egy példát. Íme egy egyszerű osztály, amely egy férfit ábrázol: 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.
}
Tegyük fel, hogy írunk egy programot, aminek meg kell határoznia, hogy két ember egypetéjű ikrek-e, vagy egyszerűen hasonlatosak. Öt jellemzőnk van: orrméret, szemszín, frizura, hegek jelenléte és DNS-teszt eredményei (az egyszerűség kedvéért egész kódként ábrázoljuk). Ön szerint ezek közül melyik tulajdonság teszi lehetővé, hogy programunk azonosítsa az egypetéjű ikreket? egyenlő és hashCode metódusok: legjobb gyakorlatok - 2Természetesen csak a DNS-vizsgálat adhat garanciát. Két embernek ugyanaz a szeme színe, a frizurája, az orra és még a sebhelyei is lehetnek – nagyon sok ember él a világon, és lehetetlen garantálni, hogy nincsenek kételyek. De szükségünk van egy megbízható mechanizmusra: csak a DNS-teszt eredménye alapján vonhatunk le pontos következtetést. Mit jelent ez a mi módszerünkre nézve equals()? Felül kell írnunk aManosztályban, figyelembe véve a programunk követelményeit. A módszernek össze kell hasonlítania int dnaCodea két objektum mezőjét. Ha egyenlőek, akkor a tárgyak egyenlőek.

@Override
public boolean equals(Object o) {
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
Tényleg ilyen egyszerű? Nem igazán. Valamit figyelmen kívül hagytunk. Objektumaink esetében csak egy olyan mezőt azonosítottunk, amely releváns az objektum egyenlőség megállapításához: dnaCode. Most képzeljük el, hogy nem 1, hanem 50 releváns mezőnk van. És ha két objektum mind az 50 mezője egyenlő, akkor az objektumok egyenlőek. Egy ilyen forgatókönyv is lehetséges. A fő probléma az, hogy az egyenlőség megállapítása 50 mező összehasonlításával idő- és erőforrás-igényes folyamat. Most képzeljük el, hogy az osztályunkon kívül Manvan egy Womanosztályunk is, amely pontosan ugyanazokkal a mezőkkel rendelkezik, mint a Man. Ha egy másik programozó használja az osztályainkat, könnyen írhat ilyen kódot:

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));
}
Ebben az esetben a mezőértékek ellenőrzése értelmetlen: jól láthatjuk, hogy két különböző osztályú objektumunk van, tehát semmiképpen sem lehetnek egyenlőek! Ez azt jelenti, hogy a metódushoz egy ellenőrzést kell hozzáadnunk equals(), amely összehasonlítja az összehasonlított objektumok osztályait. Jó, hogy erre gondoltunk!

@Override
public boolean equals(Object o) {
   if (getClass() != o.getClass()) return false;
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
De lehet, hogy mást is elfelejtettünk? Hmm... Legalább ellenőrizni kell, hogy nem hasonlítunk össze egy tárgyat önmagával! Ha az A és B hivatkozások ugyanarra a memóriacímre mutatnak, akkor ugyanaz az objektum, és nem kell időt vesztegetnünk 50 mező összehasonlítására.

@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;
}
Az sem árt, ha egy csekket adunk hozzá null: egyetlen objektum sem lehet egyenlő a -val null. Tehát, ha a metódus paramétere null, akkor nincs értelme a további ellenőrzéseknek. Mindezt szem előtt tartva equals()az Manosztályra vonatkozó módszerünk így néz ki:

@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;
}
Elvégezzük az összes fent említett kezdeti ellenőrzést. A nap végén, ha:
  • két azonos osztályba tartozó objektumot hasonlítunk össze
  • és az összehasonlított objektumok nem ugyanazok az objektumok
  • és az átadott objektum nemnull
...majd folytatjuk a releváns jellemzők összehasonlítását. Számunkra ez dnaCodea két objektum mezőit jelenti. A módszer felülbírálásakor equals()ügyeljen az alábbi követelmények betartására:
  1. Reflexivitás.

    Ha a equals()metódust arra használjuk, hogy bármely objektumot önmagával hasonlítson össze, akkor igazat kell adnia.
    Ennek a követelménynek már eleget tettünk. Módszerünk a következőket tartalmazza:

    
    if (this == o) return true;
    

  2. Szimmetria.

    Ha a.equals(b) == true, akkor b.equals(a)vissza kell térnie true.
    Módszerünk ennek a követelménynek is megfelel.

  3. Tranzitivitás.

    Ha két objektum egyenlő valamelyik harmadik tárggyal, akkor egyenlőnek kell lenniük egymással.
    Ha a.equals(b) == trueés a.equals(c) == true, akkor b.equals(c)is igazat kell adnia.

  4. Kitartás.

    A eredménynek equals()csak akkor kell változnia, ha az érintett mezők megváltoznak. Ha a két objektum adatai nem változnak, akkor az eredménynek equals()mindig azonosnak kell lennie.

  5. Egyenlőtlenség -val null.

    Minden objektum esetén a.equals(null)hamis értéket kell visszaadnia
    . Ez nem csak néhány "hasznos ajánlás", hanem egy szigorú szerződés , amelyet az Oracle dokumentációja tartalmaz.

hashCode() metódus

Most beszéljünk a módszerről hashCode(). Miért van rá szükség? Pontosan ugyanerre a célra - tárgyak összehasonlítására. De már megvan equals()! Miért más módszer? A válasz egyszerű: a teljesítmény javítása. A Java nyelvben a hashCode()metódussal ábrázolt hash függvény fix hosszúságú számértéket ad vissza bármely objektumhoz. Java nyelven a metódus bármely objektumhoz hashCode()32 bites számot ( ) ad vissza . intKét szám összehasonlítása sokkal gyorsabb, mint két objektum összehasonlítása a equals()módszerrel, különösen, ha ez a módszer sok mezőt vesz figyelembe. Ha a programunk objektumokat hasonlít össze, akkor ezt sokkal egyszerűbb megtenni hash kóddal. hashCode()Az összehasonlítás csak akkor folytatódik, ha a metódus alapján az objektumok egyenlőekequals()módszer. Egyébként így működnek a hash alapú adatstruktúrák, például az ismerős HashMap! A hashCode()metódust a metódushoz hasonlóan equals()a fejlesztő felülírja. equals()A módszerhez hasonlóan hashCode()az Oracle dokumentációjában meghatározott hivatalos követelmények vannak:
  1. Ha két objektum egyenlő (vagyis a equals()metódus true értéket ad vissza), akkor azonos hash kóddal kell rendelkezniük.

    Ellenkező esetben a módszereink értelmetlenek lennének. Ahogy fentebb említettük, hashCode()a teljesítmény javítása érdekében először ellenőrizni kell. Ha a hash kódok eltérőek lennének, akkor az ellenőrzés hamis értéket adna vissza, még akkor is, ha az objektumok valójában megegyeznek a metódus meghatározása szerint equals().

  2. Ha a hashCode()metódust többször hívják ugyanazon az objektumon, akkor minden alkalommal ugyanazt a számot kell visszaadnia.

  3. Az 1. szabály ellenkező irányban nem működik. Két különböző objektumnak lehet ugyanaz a hash kódja.

A harmadik szabály kissé zavaró. Hogy lehet ez? A magyarázat meglehetősen egyszerű. A hashCode()metódus egy int. An integy 32 bites szám. Korlátozott értéktartománya van: -2 147 483 648 és +2 147 483 647 között. Más szavakkal, valamivel több mint 4 milliárd lehetséges értéke van egy int. Most képzelje el, hogy létrehoz egy programot, amely adatokat tárol a Földön élő összes emberről. Minden személy a saját objektumának felel meg Person(hasonlóan az Manosztályhoz). ~7,5 milliárd ember él a bolygón. Más szóval, nem számít, milyen ügyes algoritmust írunk a konvertáláshozPersonobjektumok egy int-hez, egyszerűen nincs elég lehetséges számunk. Csak 4,5 milliárd lehetséges int értékünk van, de ennél sokkal többen vannak. Ez azt jelenti, hogy bármennyire is igyekszünk, néhány embernek ugyanaz a hash kódja lesz. Amikor ez megtörténik (két különböző objektum hash kódjai egybeesnek), ütközésnek nevezzük. A módszer felülbírálásakor hashCode()a programozó egyik célja az ütközések lehetséges számának minimalizálása. Ha figyelembe vesszük ezeket a szabályokat, hogyan fog hashCode()kinézni a módszer az osztályban Person? Mint ez:

@Override
public int hashCode() {
   return dnaCode;
}
Meglepődött? :) Ha megnézed a követelményeket, látni fogod, hogy mindegyiknek megfelelünk. Azok az objektumok, amelyekre a metódusunk equals()igazat ad vissza, szintén egyenlőek lesznek a szerint hashCode(). Ha a két objektumunk Personegyenlő -ben equals(vagyis azonos -ban van dnaCode), akkor a metódusunk ugyanazt a számot adja vissza. Nézzünk egy nehezebb példát. Tegyük fel, hogy programunknak luxusautókat kell kiválasztania az autógyűjtők számára. A gyűjtés összetett hobbi lehet, számos sajátossággal. Egy adott 1963-as autó 100-szor többe kerülhet, mint egy 1964-es autó. Egy 1970-es piros autó 100-szor többe kerülhet, mint egy ugyanabban az évben gyártott kék autó. egyenlő és hashCode metódusok: legjobb gyakorlatok - 4Az előző példánkban az osztállyal Persona legtöbb mezőt (azaz az emberi jellemzőket) elvettük jelentéktelennek, és csak adnaCodemező az összehasonlításokban. Most egy nagyon egyedi területen dolgozunk, amelyben nincsenek jelentéktelen részletek! Íme az osztályunk LuxuryAuto:

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.
}
Most az összehasonlításunk során minden területet figyelembe kell vennünk. Bármilyen hiba több százezer dollárba kerülhet az ügyfélnek, ezért jobb, ha túlzottan biztonságban vagyunk:

@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);
}
Módszerünkben equals()nem felejtettük el az összes ellenőrzést, amelyről korábban beszéltünk. De most összehasonlítjuk tárgyaink mindhárom mezőjét. Ehhez a programhoz abszolút egyenlőségre, azaz az egyes területek egyenlőségére van szükség. Mi a helyzet hashCode?

@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = result + manufactureYear;
   result = result + dollarPrice;
   return result;
}
modelA mi osztályunkban a mező egy String. Ez kényelmes, mert az Stringosztály már felülírja a hashCode()metódust. Kiszámoljuk a modelmező hash kódját, majd hozzáadjuk a másik két numerikus mező összegét. A Java fejlesztőknek van egy egyszerű trükkje, amellyel csökkenthetik az ütközések számát: a hash kód kiszámításakor a köztes eredményt szorozzák meg egy páratlan prímszámmal. A leggyakrabban használt szám a 29 vagy a 31. A matematikai finomságokba most nem megyünk bele, de a jövőben ne feledjük, hogy a köztes eredmények kellően nagy páratlan számmal való szorzása segít a hash függvény eredményeinek "kiterítésében", ill. következésképpen csökkentse az azonos hash kóddal rendelkező objektumok számát. A LuxuryAuto módszerünknél hashCode()ez így nézne ki:

@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = 31 * result + manufactureYear;
   result = 31 * result + dollarPrice;
   return result;
}
Ennek a mechanizmusnak a bonyolultságáról többet olvashat ebben a StackOverflow bejegyzésben , valamint Joshua Bloch Hatékony Java című könyvében. Végül még egy fontos szempont, amelyet érdemes megemlíteni. Minden alkalommal, amikor felülírtuk az equals()and hashCode()metódust, kiválasztottunk bizonyos példánymezőket, amelyeket ezek a metódusok figyelembe vesznek. Ezek a módszerek ugyanazokat a mezőket veszik figyelembe. De figyelembe vehetünk-e különböző területeket equals()a és -ben hashCode()? Technikailag megtehetjük. De ez egy rossz ötlet, és íme, miért:

@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;
}
Itt vannak a mi equals()és hashCode()a módszereink az osztályhoz LuxuryAuto. A hashCode()metódus változatlan maradt, de a mezőt eltávolítottuk modela equals()metódusból. A modell már nem jellemző, amikor a equals()módszer két objektumot hasonlít össze. De a hash kód kiszámításakor ez a mező továbbra is figyelembe vehető. Mit kapunk ennek eredményeként? Alkossunk két autót, és megtudjuk!

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
Hiba! equals()A és módszerekhez különböző mezők használatával hashCode()megszegtük a velük kötött szerződéseket! Két, a metódus szerint egyenlő objektumnak equals()azonos hash kóddal kell rendelkeznie. Más értékeket kaptunk értük. Az ilyen hibák teljesen hihetetlen következményekkel járhatnak, különösen, ha hash-t használó gyűjteményekkel dolgozunk. equals()Ennek eredményeként a és felülírásakor hashCode()ugyanazokat a mezőket kell figyelembe vennie. Ez a lecke meglehetősen hosszú volt, de ma sokat tanultál! :) Itt az ideje, hogy visszatérjünk a feladatok megoldásához!
Hozzászólások
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION