Java er lig med () og hashCode () -kontrakter

1. Oversigt

I denne vejledning introducerer vi to metoder, der hører tæt sammen: lige med() og hashCode (). Vi fokuserer på deres forhold til hinanden, hvordan man korrekt tilsidesætter dem, og hvorfor vi skal tilsidesætte begge eller ingen af ​​dem.

2. lige med()

Det Objekt klasse definerer både lige med() og hashCode () metoder - hvilket betyder, at disse to metoder er implicit defineret i hver Java-klasse, inklusive dem, vi opretter:

klasse Penge {int beløb; String currencyCode; }
Pengeindkomst = nye penge (55, "USD"); Pengeudgifter = nye penge (55, "USD"); boolsk afbalanceret = indkomst. ligestilling (udgifter)

Vi ville forvente indtægter. ligestilling (udgifter) at vende tilbage rigtigt. Men med Penge klasse i sin nuværende form, vil det ikke.

Standardimplementeringen af lige med() i klassen Objekt siger, at lighed er det samme som objektidentitet. Og indkomst og udgifter er to forskellige tilfælde.

2.1. Tilsidesættelse lige med()

Lad os tilsidesætte lige med() metode, så den ikke kun betragter objektidentitet, men snarere også værdien af ​​de to relevante egenskaber:

@Override offentlige boolske er lig med (Objekt o) hvis (o == dette) returnerer sandt; hvis (! (o. penge)) returnerer falsk; Penge andre = (Penge) o; boolsk currencyCodeEquals = (this.currencyCode == null && other.currencyCode == null) 

2.2. lige med() Kontrakt

Java SE definerer en kontrakt, som vores implementering af lige med() metoden skal opfylde. De fleste af kriterierne er sund fornuft. Det lige med() metoden skal være:

  • refleksiv: et objekt skal være lig med sig selv
  • symmetrisk: x.equals (y) skal returnere det samme resultat som y.equals (x)
  • transitiv: hvis x.equals (y) og y.equals (z) så også x.equals (z)
  • konsekvent: værdien af lige med() bør kun ændre sig, hvis en ejendom, der er indeholdt i lige med() ændringer (ingen tilfældighed tilladt)

Vi kan slå de nøjagtige kriterier op i Java SE Docs til Objekt klasse.

2.3. Overtrædelse lige med() Symmetri med arv

Hvis kriterierne for lige med() er sådan sund fornuft, hvordan kan vi overtræde den overhovedet? Godt, overtrædelser sker oftest, hvis vi udvider en klasse, der har tilsidesat lige med(). Lad os overveje en Rabatkupon klasse, der udvider vores Penge klasse:

klasse WrongVoucher udvider penge {privat strengbutik; @Override offentlige boolske lig (Objekt o) // andre metoder}

Ved første øjekast, Rabatkupon klasse og dens tilsidesættelse for lige med() synes at være korrekt. Og begge dele lige med() metoder opfører sig korrekt, så længe vi sammenligner Penge til Penge eller Rabatkupon til Rabatkupon. Men hvad sker der, hvis vi sammenligner disse to objekter?

Penge kontant = nye penge (42, "USD"); WrongVoucher voucher = ny WrongVoucher (42, "USD", "Amazon"); voucher.equals (kontant) => falsk // Som forventet. cash.equals (voucher) => sand // Det er forkert.

Det overtræder symmetri kriterierne for lige med() kontrakt.

2.4. Lave lige med() Symmetri med komposition

For at undgå denne faldgrube bør vi favoriserer sammensætning frem for arv.

I stedet for at underklasse Penge, lad os oprette en Rabatkupon klasse med en Penge ejendom:

klasse Voucher {private penge værdi; privat String butik; Kupon (int-beløb, String currencyCode, String-butik) {this.value = nye penge (beløb, currencyCode); this.store = butik; } @ Override offentlige boolske lig (Objekt o) // andre metoder}

Og nu, lige med fungerer symmetrisk, som kontrakten kræver.

3. hashCode ()

hashCode () returnerer et heltal, der repræsenterer den aktuelle forekomst af klassen. Vi skal beregne denne værdi i overensstemmelse med definitionen af ​​lighed for klassen. Dermed hvis vi tilsidesætter lige med() metode, skal vi også tilsidesætte hashCode ().

For nogle flere detaljer, se vores guide til hashCode ().

3.1. hashCode () Kontrakt

Java SE definerer også en kontrakt for hashCode () metode. Et grundigt kig på det viser, hvor nært beslægtet hashCode () og lige med() er.

Alle tre kriterier i kontrakten om hashCode () nævne på nogle måder lige med() metode:

  • intern konsistens: værdien af hashCode () kan kun ændre sig, hvis en ejendom, der er i lige med() ændringer
  • er lig med konsistens: objekter, der er lig med hinanden, skal returnere den samme hashCode
  • kollisioner: ulige objekter kan have den samme hashCode

3.2. Krænker konsistensen af hashCode () og lige med()

De 2. kriterier i hashCode-metodekontrakten har en vigtig konsekvens: Hvis vi tilsidesætter lig med (), skal vi også tilsidesætte hashCode (). Og dette er langt den mest udbredte overtrædelse med hensyn til kontrakter fra lige med() og hashCode () metoder.

Lad os se et sådant eksempel:

klasse Team {Strengby; String afdeling; @Override offentlige endelige boolske lig (Objekt o) {// implementering}}

Det Hold klasse tilsidesætter kun lige med(), men det bruger stadig implicit standardimplementeringen af hashCode () som defineret i Objekt klasse. Og dette returnerer en anden hashCode () for hver forekomst af klassen. Dette overtræder den anden regel.

Nu hvis vi opretter to Hold objekter, både med byen "New York" og afdeling "marketing", de vil være lige, men de returnerer forskellige hashCodes.

3.3. HashMap Nøgle med en inkonsekvent hashCode ()

Men hvorfor er kontraktovertrædelsen i vores Hold klasse et problem? Problemet starter, når nogle hash-baserede samlinger er involveret. Lad os prøve at bruge vores Hold klasse som en nøgle til en HashMap:

Kortledere = nyt HashMap (); ledere.put (nyt team ("New York", "udvikling"), "Anne"); ledere.put (nyt team ("Boston", "udvikling"), "Brian"); ledere.put (nyt team ("Boston", "marketing"), "Charlie"); Team myTeam = nyt team ("New York", "udvikling"); String myTeamLeader = ledere.get (myTeam);

Vi ville forvente myTeamLeader at returnere "Anne". Men med den nuværende kode gør det ikke.

Hvis vi vil bruge forekomster af Hold klasse som HashMap nøgler, vi er nødt til at tilsidesætte hashCode () metode, så den overholder kontrakten: Lige objekter returnerer det samme hashCode.

Lad os se et eksempel på implementering:

@ Override offentlig endelig int hashCode () {int resultat = 17; hvis (by! = null) {resultat = 31 * resultat + by.hashCode (); } hvis (afdeling! = null) {resultat = 31 * resultat + department.hashCode (); } returnere resultat }

Efter denne ændring, ledere.get (myTeam) returnerer "Anne" som forventet.

4. Hvornår tilsidesætter vi? lige med() og hashCode ()?

Generelt vil vi tilsidesætte enten dem begge eller ingen af ​​dem. Vi har lige set i afsnit 3 de uønskede konsekvenser, hvis vi ignorerer denne regel.

Domænedrevet design kan hjælpe os med at beslutte omstændigheder, hvornår vi skal lade dem være. For enhedsklasser - for objekter, der har en egen identitet - giver standardimplementeringen ofte mening.

Imidlertid, for værdiobjekter foretrækker vi normalt lighed baseret på deres egenskaber. Således ønsker at tilsidesætte lige med() og hashCode (). Husk vores Penge klasse fra afsnit 2: 55 USD svarer til 55 USD - selvom de er to separate forekomster.

5. Implementeringshjælpere

Vi skriver typisk ikke implementeringen af ​​disse metoder i hånden. Som det kan ses, er der en hel del faldgruber.

En almindelig måde er at lade vores IDE generere lige med() og hashCode () metoder.

Apache Commons Lang og Google Guava har hjælperklasser for at forenkle skrivning af begge metoder.

Project Lombok leverer også en @EqualsAndHashCode kommentar. Bemærk igen hvordan lige med() og hashCode () "Gå sammen" og endda få en fælles kommentar.

6. Kontrol af kontrakterne

Hvis vi vil kontrollere, om vores implementeringer overholder Java SE-kontrakterne og også nogle bedste praksis, vi kan bruge EqualsVerifier-biblioteket.

Lad os tilføje EqualsVerifier Maven testafhængighed:

 nl.jqno.equalsverifier equalsverifier 3.0.3 test 

Lad os kontrollere, at vores Hold klasse følger lige med() og hashCode () kontrakter:

@Test offentlig ugyldighed er lig medHashCodeContracts () {EqualsVerifier.forClass (Team.class) .verify (); }

Det er værd at bemærke det EqualsVerifier tester både lige med() og hashCode () metoder.

EqualsVerifier er meget strengere end Java SE-kontrakten. For eksempel sørger det for, at vores metoder ikke kan kaste et NullPointerException. Det håndhæver også, at begge metoder eller selve klassen er endelige.

Det er vigtigt at indse det standardkonfigurationen af EqualsVerifier tillader kun uforanderlige felter. Dette er en strengere kontrol end hvad Java SE-kontrakten tillader. Dette overholder en anbefaling fra Domain-Driven Design for at gøre værdigenstande uforanderlige.

Hvis vi finder nogle af de indbyggede begrænsninger unødvendige, kan vi tilføje en undertryk (Advarsel.SPECIFIC_WARNING) til vores EqualsVerifier opkald.

7. Konklusion

I denne artikel har vi diskuteret lige med() og hashCode () kontrakter. Vi skal huske at:

  • Tilsidesæt altid hashCode () hvis vi tilsidesætter lige med()
  • Tilsidesæt lige med() og hashCode () for værdiobjekter
  • Vær opmærksom på fælderne i at udvide klasser, der er tilsidesat lige med() og hashCode ()
  • Overvej at bruge en IDE eller et tredjepartsbibliotek til at generere lige med() og hashCode () metoder
  • Overvej at bruge EqualsVerifier til at teste vores implementering

Endelig kan alle kodeeksempler findes på GitHub.