Java kontrakty equals () a hashCode ()

1. Prehľad

V tomto tutoriáli si predstavíme dve metódy, ktoré k sebe úzko patria: rovná sa () a hashCode (). Zameriame sa na ich vzájomný vzťah, na to, ako ich správne prepísať a prečo by sme mali prepísať oboje alebo žiadne.

2. rovná sa ()

The Objekt trieda definuje ako rovná sa () a hashCode () methods - čo znamená, že tieto dve metódy sú implicitne definované v každej triede Java vrátane tých, ktoré vytvárame:

trieda Peniaze {int suma; Reťazec currencyCode; }
Peňažný príjem = nové peniaze (55 USD); Peňažné výdavky = nové peniaze (55 USD); boolean vyvážený = príjem.rovná (výdavky)

Očakávali by sme príjem.equals (výdavky) vrátiť sa pravda. Ale s Peniaze triedy v súčasnej podobe, nebude.

Predvolená implementácia rovná sa () v triede Objekt hovorí, že rovnosť je rovnaká ako totožnosť objektu. A príjem a výdavky sú dva odlišné prípady.

2.1. Naliehavé rovná sa ()

Prepíšeme rovná sa () metóda, ktorá nezohľadňuje iba identitu objektu, ale aj hodnotu dvoch relevantných vlastností:

@ Override public boolean equals (Object o) if (o == this) return true; if (! (o instanceof Money)) vráti false; Peniaze iné = (Peniaze) o; boolean currencyCodeEquals = (this.currencyCode == null && other.currencyCode == null) 

2.2. rovná sa () Zmluva

Java SE definuje zmluvu, ktorú naša implementácia rovná sa () metóda musí spĺňať. Väčšina kritérií je zdravý rozum. The rovná sa () metóda musí byť:

  • reflexívne: objekt sa musí rovnať
  • symetrický: x.equals (y) musí vrátiť rovnaký výsledok ako y.equals (x)
  • tranzitívny: ak x.equals (y) a y.equals (z) potom tiež x.equals (z)
  • dôsledný: hodnota rovná sa () by sa malo zmeniť, iba ak je vlastnosť, ktorá je obsiahnutá v rovná sa () zmeny (nie je povolená náhodnosť)

Môžeme vyhľadať presné kritériá v dokumente Java SE pre Objekt trieda.

2.3. Porušujúce rovná sa () Symetria s dedičstvom

Ak sú splnené kritériá pre rovná sa () je taký zdravý rozum, ako ho môžeme vôbec porušiť? No, porušenia sa vyskytujú najčastejšie, ak rozšírime triedu, ktorá prepísala rovná sa (). Uvažujme a Poukážka trieda, ktorá rozširuje našu Peniaze trieda:

trieda WrongVoucher rozširuje Money {private String store; @ Override public boolean equals (Object o) // ďalšie metódy}

Na prvý pohľad Poukážka triedy a jej prepísanie pre rovná sa () sa javia ako správne. A oboje rovná sa () metódy sa správajú správne, pokiaľ porovnávame Peniaze do Peniaze alebo Poukážka do Poukážka. Čo sa však stane, ak porovnáme tieto dva objekty?

Peňažná hotovosť = nové peniaze (42 USD); Poukaz WrongVoucher = nový WrongVoucher (42, „USD“, „Amazon“); voucher.equals (cash) => false // Podľa očakávania. cash.equals (voucher) => true // To je zle.

To porušuje kritériá symetrie normy rovná sa () zmluva.

2.4. Upevnenie rovná sa () Symetria s kompozíciou

Aby sme sa vyhli tomuto úskaliu, mali by sme uprednostniť zloženie pred dedičstvom.

Namiesto podtriedy Peniaze, vytvorme a Poukážka trieda s a Peniaze nehnuteľnosť:

poukaz na triedu {hodnota súkromných peňazí; súkromný obchod String; Voucher (čiastka int, reťazec currencyCode, obchod reťazca) {this.value = nové peniaze (čiastka, currencyCode); this.store = store; } @Override public boolean equals (Object o) // ďalšie metódy}

A teraz, rovná sa bude fungovať symetricky, ako to vyžaduje zmluva.

3. hashCode ()

hashCode () vráti celé číslo predstavujúce aktuálnu inštanciu triedy. Túto hodnotu by sme mali vypočítať v súlade s definíciou rovnosti pre triedu. Teda ak prepíšeme rovná sa () metódou, musíme tiež prepísať hashCode ().

Ďalšie podrobnosti nájdete v našom sprievodcovi po hashCode ().

3.1. hashCode () Zmluva

Java SE tiež definuje zmluvu pre hashCode () metóda. Dôkladný pohľad na to ukazuje, ako úzko súvisia hashCode () a rovná sa () sú.

Všetky tri kritériá uvedené v zmluve hashCode () v niektorých ohľadoch spomenúť rovná sa () metóda:

  • vnútorná konzistencia: hodnota hashCode () sa môže zmeniť, iba ak sa nehnuteľnosť nachádza v rovná sa () zmeny
  • rovná sa konzistencia: objekty, ktoré sú si navzájom rovnocenné, musia vrátiť rovnaký hashCode
  • kolízie: nerovné objekty môžu mať rovnaký hashCode

3.2. Porušenie jednotnosti hashCode () a rovná sa ()

Druhé kritérium zmluvy s metódami hashCode má dôležitý dôsledok: Ak prepíšeme equals (), musíme prepísať aj hashCode (). A toto je zďaleka najrozšírenejšie porušenie, čo sa týka zmlúv rovná sa () a hashCode () metódy.

Pozrime sa na taký príklad:

trieda Tím {Mesto reťazcov; Sláčikové oddelenie; @Override public final boolean equals (Objekt o) {// implementácia}}

The Tím iba prepísania triedy rovná sa (), ale stále implicitne používa predvolenú implementáciu hashCode () ako je definované v Objekt trieda. A toto vracia iné hashCode () pre každú inštanciu triedy. Porušuje sa tým druhé pravidlo.

Teraz, ak vytvoríme dve Tím s mestom „New York“ a oddelením „marketing“ budú rovnaké, ale vrátia rôzne hashCody.

3.3. HashMap Kľúč s nekonzistentným hashCode ()

Prečo je to však porušenie zmluvy v našom Tím trieda problém? Problémy však začínajú, keď sa jedná o niektoré zbierky založené na hashe. Skúsme použiť našu Tím triedy ako kľúč a HashMap:

Vodcovia máp = nový HashMap (); leader.put (nový tím („New York“, „vývoj“), „Anne“); leader.put (nový tím („Boston“, „vývoj“), „Brian“); leader.put (nový tím („Boston“, „marketing“), „Charlie“); Team myTeam = nový tím ("New York", "vývoj"); Reťazec myTeamLeader = leader.get (myTeam);

Očakávali by sme myTeamLeader vrátiť „Anne“. Ale pri súčasnom kóde nie.

Ak chceme použiť inštancie Tím trieda ako HashMap kľúče, musíme prepísať hashCode () spôsobom, ktorý dodržiava zmluvu: Rovnaké objekty vrátia to isté hashCode.

Pozrime sa na príklad implementácie:

@ Overenie verejné konečné int hashCode () {int výsledok = 17; if (city! = null) {result = 31 * result + city.hashCode (); } if (department! = null) {result = 31 * result + department.hashCode (); } vrátiť výsledok; }

Po tejto zmene leader.get (myTeam) vráti „Anne“ podľa očakávania.

4. Kedy prepíšeme rovná sa () a hashCode ()?

Spravidla chceme prepísať buď obidve, alebo ani jedno z nich. V časti 3 sme práve videli nežiaduce dôsledky, ak ignorujeme toto pravidlo.

Návrh založený na doménach nám môže pomôcť rozhodnúť sa o okolnostiach, kedy by sme ich mali nechať. Pre triedy entít - pre objekty, ktoré majú vnútornú identitu - má často predvolená implementácia zmysel.

Avšak pre hodnotové objekty zvyčajne uprednostňujeme rovnosť na základe ich vlastností. Preto chcete prepísať rovná sa () a hashCode (). Pamätajte na našu Peniaze trieda z časti 2: 55 USD sa rovná 55 USD - aj keď ide o dva samostatné prípady.

5. Pomocníci pri implementácii

Implementáciu týchto metód zvyčajne nepíšeme ručne. Ako vidno, nástrah je pomerne veľa.

Jedným z bežných spôsobov je nechať náš IDE generovať rovná sa () a hashCode () metódy.

Apache Commons Lang a Google Guava majú triedy pomocníkov, aby zjednodušili písanie oboch metód.

Projekt Lombok tiež poskytuje @EqualsAndHashCode anotácia. Znova si všimnite, ako rovná sa () a hashCode () „Ísť spolu“ a mať dokonca spoločnú anotáciu.

6. Overenie zmlúv

Ak chceme skontrolovať, či naše implementácie dodržiavajú zmluvy Java SE a tiež niektoré osvedčené postupy, môžeme použiť knižnicu EqualsVerifier.

Pridajme závislosť testu EqualsVerifier Maven:

 nl.jqno.equalsverifier test equalsverifier 3.0.3 

Poďme si to overiť Tím trieda nasleduje po rovná sa () a hashCode () zmluvy:

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

Stojí za zmienku, že EqualsVerifier testuje obe rovná sa () a hashCode () metódy.

EqualsVerifier je oveľa prísnejšia ako zmluva Java SE. Napríklad zaisťuje, že naše metódy nemôžu vyhodiť a NullPointerException. Tiež vynucuje, aby boli obidve metódy alebo trieda samotná konečná.

Je dôležité si to uvedomiť predvolená konfigurácia EqualsVerifier umožňuje iba nemenné polia. Toto je prísnejšia kontrola, ako povoľuje zmluva Java SE. Toto je v súlade s odporúčaním Domain-Driven Design, aby boli hodnotné objekty nemenné.

Ak považujeme niektoré zo zabudovaných obmedzení za zbytočné, môžeme pridať a potlačiť (Warning.SPECIFIC_WARNING) k nášmu EqualsVerifier hovor.

7. Záver

V tomto článku sme diskutovali o rovná sa () a hashCode () zmluvy. Mali by sme pamätať na:

  • Vždy prepísať hashCode () ak prepíšeme rovná sa ()
  • Prepísať rovná sa () a hashCode () pre hodnotové objekty
  • Uvedomte si nástrahy rozširujúcich sa tried, ktoré boli prepísané rovná sa () a hashCode ()
  • Zvážte použitie IDE alebo knižnice tretej strany na generovanie rovná sa () a hashCode () metódy
  • Zvážte použitie nástroja EqualsVerifier na otestovanie našej implementácie

Nakoniec všetky príklady kódov nájdete na GitHub.