A Java egyenlő () és hashCode () szerződésekkel

1. Áttekintés

Ebben az oktatóanyagban két, egymáshoz szorosan kapcsolódó módszert mutatunk be: egyenlő () és hash kód(). Arra fogunk összpontosítani, hogy hogyan viszonyulnak egymáshoz, hogyan kell helyesen felülbírálni őket, és miért kellene felülírnunk mindkettőt, vagy egyiket sem.

2. egyenlő ()

A Tárgy osztály meghatározza mind a egyenlő () és hash kód() módszerek - ami azt jelenti, hogy ez a két módszer implicit módon meg van határozva minden Java osztályban, beleértve az általunk létrehozottakat is:

osztály Pénz {int összeg; String currencyCode; }
Pénzbevétel = új pénz (55, "USD"); Pénzköltségek = új pénz (55, "USD"); logikai kiegyensúlyozott = jövedelem. egyenlő (ráfordítás)

Várnánk jövedelem. egyenlőség (ráfordítás) visszatérni igaz. De a Pénz osztály a jelenlegi formájában, nem fog.

A. Alapértelmezett megvalósítása egyenlő () az osztályban Tárgy azt mondja, hogy az egyenlőség megegyezik a tárgyi identitással. És jövedelem és költségek két különálló eset.

2.1. Felülbíráló egyenlő ()

Írjuk felül egyenlő () metódust, hogy ne csak az objektum azonosságát vegye figyelembe, hanem a két releváns tulajdonság értékét is:

@Orride public boolean egyenlő (Object o), ha (o == this) return true; ha (! (a pénz példánya)) hamis értéket ad vissza; Pénz egyéb = (Pénz) o; logikai pénznemCodeEquals = (this.currencyCode == null && other.currencyCode == null) 

2.2. egyenlő () Szerződés

A Java SE meghatározza azt a szerződést, amelyet a egyenlő () módszer teljesülnie kell. A legtöbb kritérium az józan ész. A egyenlő () A módszernek a következőknek kell lennie:

  • visszaható: egy tárgynak meg kell egyeznie önmagával
  • szimmetrikus: x.egyenlő (y) ugyanazt az eredményt kell adnia, mint y.egyenlő (x)
  • tranzitív: ha x.egyenlő (y) és y. egyenlő (z) akkor is x.egyenlő (z)
  • következetes: az értéke egyenlő () csak akkor változhat meg, ha a egyenlő () változások (véletlenszerűség nem megengedett)

Megnézhetjük a pontos kritériumokat a Java SE Docs-ban a Tárgy osztály.

2.3. Megsértő egyenlő () Szimmetria örökléssel

Ha a kritériumok egyenlő () ilyen józan ész, hogyan sérthetjük meg egyáltalán? Jól, a szabálysértések akkor fordulnak elő leggyakrabban, ha kiterjesztünk egy osztályt, amely felülírta egyenlő (). Vegyük fontolóra a Utalvány osztály, amely kiterjeszti a mi Pénz osztály:

osztály WrongVoucher kiterjeszti a Money {private String áruházat; @Orride public boolean equals (o objektum) // egyéb módszerek}

Első pillantásra a Utalvány osztály és annak felülírása egyenlő () úgy tűnik, hogy helyes. És mindkettő egyenlő () a módszerek helyesen viselkednek, amíg összehasonlítjuk Pénz nak nek Pénz vagy Utalvány nak nek Utalvány. De mi történik, ha összehasonlítjuk ezt a két objektumot?

Pénz készpénz = új pénz (42, "USD"); WrongVoucher utalvány = új WrongVoucher (42, "USD", "Amazon"); utalvány.egyenlő (készpénz) => hamis // A várakozásoknak megfelelően. cash.equals (utalvány) => true // Ez helytelen.

Ez sérti a egyenlő () szerződés.

2.4. Rögzítő egyenlő () Szimmetria összetételével

Ennek a csapdának elkerülése érdekében meg kell tennünk az öröklés helyett a kompozíciót részesíti előnyben.

Alosztályozás helyett Pénz, hozzunk létre egy Utalvány osztály a Pénz ingatlan:

osztályú utalvány {magán pénz értéke; saját String üzlet; Utalvány (int összeg, String currencyCode, String bolt) {this.value = új Pénz (összeg, currencyCode); this.store = bolt; } @Orride public boolean egyenlő (o objektum) // egyéb módszerek}

És most, egyenlő szimmetrikusan fog működni, ahogy a szerződés megköveteli.

3. hash kód()

hash kód() egész számot ad vissza, amely az osztály aktuális példányát képviseli. Ezt az értéket az osztály egyenlőségének meghatározásával összhangban kell kiszámítanunk. Így ha felülírjuk a egyenlő () módszerrel is felül kell írnunk hash kód().

További részletekért nézze meg útmutatónkat hash kód().

3.1. hash kód() Szerződés

A Java SE meghatározza a hash kód() módszer. Alapos áttekintése megmutatja, hogy mennyire szoros kapcsolatban állnak egymással hash kód() és egyenlő () vannak.

A szerződés mindhárom kritériuma hash kód() említsen bizonyos szempontból a egyenlő () módszer:

  • belső konzisztenciája: az értéke hash kód() csak akkor változhat, ha a egyenlő () változtatások
  • egyenlő a következetességgel: az egymással egyenlő objektumoknak ugyanazt a hashCode-ot kell visszaadniuk
  • ütközések: az egyenlőtlen objektumoknak ugyanaz a hashCode-ja lehet

3.2. A következetesség megsértése hash kód() és egyenlő ()

A hashCode módszerek szerződésének 2. kritériumának fontos következménye van: Ha felülírjuk az egyenlőt (), akkor felül kell írnunk a hashCode () -t is. És ez messze a legelterjedtebb szabálysértés a egyenlő () és hash kód() mód.

Lássunk egy ilyen példát:

osztály Csapat {String város; Vonós részleg; @Orride public final boolean egyenlő (o objektum) {// implementáció}}

A Csapat osztály felülírja csak egyenlő (), de még mindig implicit módon használja a hash kód() meghatározása szerint Tárgy osztály. És ez egy mást eredményez hash kód() az osztály minden példányára. Ez sérti a második szabályt.

Most ha kettőt hozunk létre Csapat tárgyak, mind a „New York” városban, mind a „marketing” osztályban, egyenlőek lesznek, de különböző hash-kódokat adnak vissza.

3.3. HashMap Kulcs Inkonzisztens hash kód()

De miért van a szerződésszegés a mi Csapat osztály probléma? Nos, a baj akkor kezdődik, amikor néhány hash-alapú gyűjtemény részt vesz. Próbáljuk meg használni a mi Csapat osztály, mint a kulcsa HashMap:

Térkép vezetők = új HashMap (); vezetők.put (új csapat ("New York", "fejlesztés"), "Anne"); vezetők.put (új csapat ("Boston", "fejlesztés"), "Brian"); vezetők.put (új csapat ("Boston", "marketing"), "Charlie"); MyTeam csapat = új csapat ("New York", "fejlesztés"); Karakterlánc myTeamLeader = vezetők.get (myTeam);

Várnánk myTeamLeader hogy visszaküldje „Anne” -t. De a jelenlegi kóddal nem.

Ha a. Példányait akarjuk használni Csapat osztály as HashMap kulcsokat, felül kell írnunk a hash kód() módszerrel, hogy az betartsa a szerződést: Az egyenlő tárgyak ugyanazt adják vissza hash kód.

Lássunk egy megvalósítási példát:

@Orride public final int hashCode () {int result = 17; if (város! = null) {eredmény = 31 * eredmény + város.hashCode (); } if (osztály! = null) {eredmény = 31 * eredmény + részleg.hashCode (); } visszatérési eredmény; }

E változás után leader.get (myTeam) az „Anne” -t a várakozásoknak megfelelően adja vissza.

4. Mikor írjuk felül egyenlő () és hash kód()?

Általában mindkettőjüket felül akarjuk írni, vagy egyiket sem. Éppen a 3. szakaszban láttuk a nem kívánt következményeket, ha figyelmen kívül hagyjuk ezt a szabályt.

A tartományvezérelt tervezés segíthet eldönteni a körülményeket, mikor hagyjuk őket. Az entitásosztályok esetében - belső identitással rendelkező objektumok esetében - az alapértelmezett megvalósításnak gyakran van értelme.

Azonban, az értéktárgyak esetében általában a tulajdonságaik alapján részesítjük előnyben az egyenlőséget. Így felül akarja írni egyenlő () és hash kód(). Emlékezz a mi Pénz osztály a 2. szakaszból: 55 USD egyenlő 55 USD - még akkor is, ha két külön példányról van szó.

5. A megvalósítás segítői

Ezeknek a módszereknek a megvalósítását általában nem kézzel írjuk. Mint látható, jó néhány buktató van.

Az egyik általános módszer az, hogy hagyjuk IDE-nket létrehozni a egyenlő () és hash kód() mód.

Az Apache Commons Lang és a Google Guava segítő osztályokkal rendelkeznek annak érdekében, hogy egyszerűsítsék mindkét módszer megírását.

A Lombok projekt egy @EqualsAndHashCode annotáció. Ismételje meg újra, hogyan egyenlő () és hash kód() „Menjetek együtt”, és legyen még közös jegyzetük is.

6. A szerződések ellenőrzése

Ha meg akarjuk vizsgálni, hogy megvalósításunk betartja-e a Java SE szerződéseket és néhány bevált gyakorlatot, használhatjuk az EqualsVerifier könyvtárat.

Adjuk hozzá az EqualsVerifier Maven teszt függőségét:

 nl.jqno.equalsverifier equalsverifier 3.0.3 teszt 

Ellenőrizzük, hogy a mi Csapat osztály követi a egyenlő () és hash kód() szerződések:

@Test public void equalsHashCodeContracts () {EqualsVerifier.forClass (Team.class) .verify (); }

Érdemes ezt megjegyezni EqualsVerifier teszteli mind a egyenlő () és hash kód() mód.

EqualsVerifier sokkal szigorúbb, mint a Java SE szerződés. Például megbizonyosodik arról, hogy módszereink nem dobhatnak-e a-t NullPointerException. Továbbá érvényesíti, hogy mindkét módszer vagy maga az osztály végleges.

Fontos ezt felismerni alapértelmezett konfigurációja EqualsVerifier csak megváltoztathatatlan mezőket engedélyez. Ez szigorúbb ellenőrzés, mint amit a Java SE szerződés megenged. Ez betartja a tartományvezérelt tervezés ajánlását az értékobjektumok megváltoztathatatlanná tételére.

Ha a beépített korlátok egy részét feleslegesnek találjuk, hozzáadhatunk egy a-t elnyomás (Warning.SPECIFIC_WARNING) a miénknek EqualsVerifier hívás.

7. Következtetés

Ebben a cikkben megvitattuk a egyenlő () és hash kód() szerződések. Nem szabad megfeledkeznünk a következőkről:

  • Mindig felülírja hash kód() ha felülírjuk egyenlő ()
  • Felülbírálás egyenlő () és hash kód() értéktárgyakhoz
  • Legyen tisztában a kibővített osztályok csapdáival, amelyek felülírtak egyenlő () és hash kód()
  • Fontolja meg egy IDE vagy egy harmadik fél könyvtárának használatát a egyenlő () és hash kód() mód
  • Fontolja meg az EqualsVerifier használatát a megvalósítás teszteléséhez

Végül az összes kódpélda megtalálható a GitHubon.