Java je jednaka () i hashCode () ugovorima

1. Pregled

U ovom uputstvu predstavit ćemo dvije metode koje međusobno usko pripadaju: jednako () i hashCode (). Usredotočit ćemo se na njihov međusobni odnos, kako ih ispravno nadjačati i zašto bismo trebali nadjačati oboje ili niti jedno ni drugo.

2. jednako ()

The Objekt razred definira i jednako () i hashCode () metode - što znači da su ove dvije metode implicitno definirane u svakoj Java klasi, uključujući i one koje stvaramo:

klasa Novac {int iznos; String currencyCode; }
Novčani prihod = novi novac (55, "USD"); Novčani troškovi = novi novac (55, "USD"); logička uravnoteženost = prihod.equals (troškovi)

Očekivali bismo prihod.equals (troškovi) vratiti pravi. Ali s Novac klase u trenutnom obliku, neće.

Zadana implementacija jednako () u razredu Objekt kaže da je jednakost isto što i identitet predmeta. I prihod i troškovi dva su različita slučaja.

2.1. Nadjačavanje jednako ()

Zamijenimo jednako () metoda tako da ne uzima u obzir samo identitet objekta, već i vrijednost dvaju relevantnih svojstava:

@Override public boolean equals (Object o) if (o == this) return true; if (! (o instance of Money)) return false; Novac ostalo = (Novac) o; boolean currencyCodeEquals = (this.currencyCode == null && other.currencyCode == null) 

2.2. jednako () Ugovor

Java SE definira ugovor koji naša implementacija jednako () metoda mora ispuniti. Većina kriterija su zdrav razum. The jednako () metoda mora biti:

  • refleksni: objekt se mora izjednačiti
  • simetričan: x.equals (y) mora vratiti isti rezultat kao y.equals (x)
  • prijelazni: ako x.equals (y) i y.equals (z) onda također x.equals (z)
  • dosljedan: vrijednost jednako () treba promijeniti samo ako je svojstvo sadržano u jednako () promjene (nije dopuštena slučajnost)

Točne kriterije možemo potražiti u dokumentima Java SE za Objekt razred.

2.3. Kršeći jednako () Simetrija s nasljeđivanjem

Ako su kriteriji za jednako () je li takav zdrav razum, kako ga uopće možemo kršiti? Dobro, kršenja se događaju najčešće, ako proširimo klasu koja je nadjačana jednako (). Razmotrimo a Vaučer razred koji proširuje naš Novac razred:

klasa WrongVoucher proširuje Money {private String store; @Override public boolean equals (Objekt o) // ostale metode}

Na prvi pogled, Vaučer klase i njezino nadjačavanje za jednako () čini se točnim. I jedno i drugo jednako () metode ponašaju ispravno dok god uspoređujemo Novac do Novac ili Vaučer do Vaučer. Ali što će se dogoditi ako usporedimo ova dva predmeta?

Novac u gotovini = novi novac (42, "USD"); WrongVoucher vaučer = novi WrongVoucher (42, "USD", "Amazon"); voucher.equals (gotovina) => false // Kao što se i očekivalo. cash.equals (voucher) => true // To je pogrešno.

To krši kriterije simetrije jednako () ugovor.

2.4. Učvršćivanje jednako () Simetrija sa kompozicijom

Da bismo izbjegli tu zamku, trebali bismo favorizirati sastav nad nasljedstvom.

Umjesto podrazreda Novac, stvorimo a Vaučer razred s a Novac svojstvo:

razred vaučer {vrijednost privatnog novca; privatna trgovina gudačima; Voucher (int iznos, string ValutaKod, Trgovina nizova) {this.value = novi novac (iznos, kôd valute); this.store = store; } @Override public boolean equals (Objekt o) // ostale metode}

A sada, jednako radit će simetrično kako ugovor zahtijeva.

3. hashCode ()

hashCode () vraća cijeli broj koji predstavlja trenutnu instancu klase. Ovu bismo vrijednost trebali izračunati u skladu s definicijom jednakosti za klasu. Tako ako nadjačamo jednako () metodu, također moramo nadjačati hashCode ().

Za neke više detalja pogledajte naš vodič za hashCode ().

3.1. hashCode () Ugovor

Java SE također definira ugovor za hashCode () metoda. Temeljit pogled na to pokazuje koliko su usko povezani hashCode () i jednako () jesu.

Sva tri kriterija u ugovoru od hashCode () spomenuti na neki način jednako () metoda:

  • unutarnja dosljednost: vrijednost hashCode () može se promijeniti samo ako je svojstvo koje je u jednako () promjene
  • jednaka je dosljednosti: objekti koji su međusobno jednaki moraju vratiti isti hashCode
  • sudara: nejednaki objekti mogu imati isti hashCode

3.2. Kršenje dosljednosti hashCode () i jednako ()

Drugi kriterij ugovora o metodama hashCode ima važnu posljedicu: Ako poništimo equals (), moramo također nadjačati hashCode (). A ovo je daleko najrasprostranjenije kršenje u vezi s ugovorima jednako () i hashCode () metode.

Pogledajmo takav primjer:

razredna ekipa {String city; Gudački odjel; @Premaši javnu konačnu logičku vrijednost (Objekt o) {// implementacija}}

The Tim klasa poništava samo jednako (), ali i dalje implicitno koristi zadanu implementaciju hashCode () kako je definirano u Objekt razred. A ovo vraća drugačije hashCode () za svaki primjerak klase. Ovo krši drugo pravilo.

Sad ako stvorimo dva Tim objekti, kako s gradskim "New Yorkom", tako i s odjelom "marketinga", oni će biti jednaki, ali oni će vratiti različite hash kodove.

3.3. HashMap Ključ s nedosljednim hashCode ()

Ali zašto je kršenje ugovora u našem Tim klasa problem? Pa, problem počinje kad su uključene neke zbirke temeljene na raspršivanju. Pokušajmo koristiti naše Tim razred kao ključ a HashMap:

Voditelji mapa = novi HashMap (); voditelji.put (novi tim ("New York", "razvoj"), "Anne"); Leaders.put (novi tim ("Boston", "razvoj"), "Brian"); leader.put (novi tim ("Boston", "marketing"), "Charlie"); Tim myTeam = novi tim ("New York", "razvoj"); Niz myTeamLeader = leader.get (myTeam);

Očekivali bismo myTeamLeader vratiti "Anne". Ali s trenutnim kodom nema.

Ako želimo koristiti primjere datoteke Tim razred kao HashMap tipke, moramo nadjačati hashCode () metoda tako da se pridržava ugovora: Jednaki objekti vraćaju isto hashCode.

Pogledajmo primjer implementacije:

@Override public final int hashCode () {int rezultat = 17; if (grad! = null) {rezultat = 31 * rezultat + city.hashCode (); } if (odjel! = null) {rezultat = 31 * rezultat + odjel.hashCode (); } vratiti rezultat; }

Nakon ove promjene, voditelji.get (myTeam) vraća "Anne" kako se očekivalo.

4. Kada nadjačavamo jednako () i hashCode ()?

Općenito, želimo nadvladati bilo njih, bilo njih dvije. Upravo smo vidjeli 3. odjeljak neželjene posljedice ako zanemarimo ovo pravilo.

Dizajn na temelju domene može nam pomoći da odlučimo u kojim okolnostima bismo ih trebali napustiti. Za klase entiteta - za objekte koji imaju svojstveni identitet - zadana implementacija često ima smisla.

Međutim, za vrijednosne objekte obično preferiramo jednakost na temelju njihovih svojstava. Stoga želite nadjačati jednako () i hashCode (). Sjetite se našeg Novac klase iz odjeljka 2: 55 USD jednako je 55 USD - čak i ako su dvije odvojene instance.

5. Pomoćnici u implementaciji

Implementaciju ovih metoda obično ne pišemo ručno. Kao što se može vidjeti, nemalo je zamki.

Jedan od uobičajenih načina je dopustiti našem IDE-u da generira jednako () i hashCode () metode.

Apache Commons Lang i Google Guava imaju pomoćne satove kako bi pojednostavili pisanje obje metode.

Projekt Lombok također nudi @EqualsAndHashCode bilješka. Još jednom zabilježite kako jednako () i hashCode () "Ići zajedno", pa čak i imati zajedničku napomenu.

6. Provjera ugovora

Ako želimo provjeriti pridržavaju li se naše implementacije ugovora o Java SE i nekih najboljih praksi, možemo koristiti knjižnicu EqualsVerifier.

Dodajmo ovisnost testa EqualsVerifier Maven:

 nl.jqno.equalsverifier jednak provjeri 3.0.3 test 

Provjerimo da je naš Tim razred slijedi jednako () i hashCode () ugovori:

@Test javna praznina equalsHashCodeContracts () {EqualsVerifier.forClass (Team.class) .verify (); }

Vrijedno je to napomenuti EqualsVerifier testira oba jednako () i hashCode () metode.

EqualsVerifier je mnogo stroži od ugovora o Java SE. Na primjer, osigurava da naše metode ne mogu baciti a NullPointerException. Također, nalaže da su obje metode ili sama klasa konačne.

Važno je to shvatiti zadana konfiguracija EqualsVerifier dopušta samo nepromjenjiva polja. Ovo je stroža provjera od onoga što dopušta ugovor o Java SE. Ovo se pridržava preporuke dizajna vođenog domenom kako bi objekti vrijednosti postali nepromjenjivi.

Ako smatramo da su neka ugrađena ograničenja nepotrebna, možemo dodati a potisnuti (Warning.SPECIFIC_WARNING) našem EqualsVerifier poziv.

7. Zaključak

U ovom smo članku razgovarali o jednako () i hashCode () ugovorima. Trebali bismo imati na umu da:

  • Uvijek poništi hashCode () ako nadjačamo jednako ()
  • Poništi jednako () i hashCode () za vrijednosne objekte
  • Budite svjesni zamki produženja nastave koje su nadjačane jednako () i hashCode ()
  • Razmislite o upotrebi IDE-a ili biblioteke treće strane za generiranje jednako () i hashCode () metode
  • Razmislite o upotrebi programa EqualsVerifier za testiranje naše implementacije

Napokon, svi primjeri koda mogu se naći na GitHubu.