Java er lik () og hashCode () -kontrakter
1. Oversikt
I denne opplæringen vil vi introdusere to metoder som hører tett sammen: er lik() og hashCode (). Vi vil fokusere på deres forhold til hverandre, hvordan vi kan overstyre dem riktig, og hvorfor vi bør overstyre begge eller ingen av dem.
2. er lik()
De Gjenstand klasse definerer både er lik() og hashCode () metoder - noe som betyr at disse to metodene er implisitt definert i hver Java-klasse, inkludert de vi lager:
klasse Penger {int beløp; String currencyCode; }
Pengerinntekt = nye penger (55, "USD"); Pengerutgifter = nye penger (55, "USD"); boolsk balansert = inntekt. likestilling (utgifter)
Vi forventer inntekt. likestilling (utgifter) å returnere ekte. Men med Penger klasse i sin nåværende form, vil det ikke.
Standard implementering av er lik() i klassen Gjenstand sier at likhet er det samme som objektidentitet. Og inntekt og utgifter er to forskjellige tilfeller.
2.1. Overstyring er lik()
La oss overstyre er lik() metode slik at den ikke bare tar hensyn til objektidentitet, men heller verdien av de to relevante egenskapene:
@ Overstyr offentlig boolsk lik (Objekt o) hvis (o == dette) returnerer sant; hvis (! (o forekomst av penger)) returnerer falsk; Penger andre = (Penger) o; boolsk currencyCodeEquals = (this.currencyCode == null && other.currencyCode == null)
2.2. er lik() Kontrakt
Java SE definerer en kontrakt som vår implementering av er lik() metoden må oppfylle. De fleste kriteriene er sunn fornuft. De er lik() metoden må være:
- refleksiv: et objekt må like seg selv
- symmetrisk: x.equals (y) må returnere samme resultat som y.equals (x)
- transitive: hvis x.equals (y) og y.equals (z) da også x.equals (z)
- konsistent: verdien av er lik() bør bare endres hvis en eiendom som er inneholdt i er lik() endringer (ingen tilfeldighet tillatt)
Vi kan slå opp de nøyaktige kriteriene i Java SE Docs for Gjenstand klasse.
2.3. Krenkende er lik() Symmetri med arv
Hvis kriteriene for er lik() er slik sunn fornuft, hvordan kan vi bryte den i det hele tatt? Vi vil, brudd skjer oftest, hvis vi utvider en klasse som har overstyrt er lik(). La oss vurdere en Kupong klasse som utvider vår Penger klasse:
klasse WrongVoucher utvider penger {privat strengbutikk; @Override offentlige boolske lik (Objekt o) // andre metoder}
Ved første øyekast, Kupong klasse og dens overstyring for er lik() ser ut til å være riktig. Og begge deler er lik() metoder oppfører seg riktig så lenge vi sammenligner Penger til Penger eller Kupong til Kupong. Men hva skjer hvis vi sammenligner disse to objektene?
Penger kontanter = nye penger (42, "USD"); WrongVoucher-kupong = ny WrongVoucher (42, "USD", "Amazon"); voucher.equals (kontanter) => falske // Som forventet. cash.equals (voucher) => true // Det er galt.
Det bryter med symmetri kriteriene til er lik() kontrakt.
2.4. Fikser er lik() Symmetri med komposisjon
For å unngå denne fallgruven, bør vi favoriserer sammensetning fremfor arv.
I stedet for å underklasse Penger, la oss lage en Kupong klasse med en Penger eiendom:
klasse Kupong {private Money value; privat streng butikk; Kupong (int-beløp, streng valuta-kode, streng-butikk) {this.value = nye penger (beløp, valuta-kode); this.store = butikk; } @Override offentlige boolske lik (Objekt o) // andre metoder}
Og nå, er lik vil fungere symmetrisk som kontrakten krever.
3. hashCode ()
hashCode () returnerer et helt tall som representerer gjeldende forekomst av klassen. Vi bør beregne denne verdien i samsvar med definisjonen av likhet for klassen. Og dermed hvis vi overstyrer er lik() metode, må vi også overstyre hashCode ().
For litt mer informasjon, se vår guide til hashCode ().
3.1. hashCode () Kontrakt
Java SE definerer også en kontrakt for hashCode () metode. En grundig titt på den viser hvor nært beslektet hashCode () og er lik() er.
Alle tre kriteriene i kontrakten av hashCode () nevne på noen måter er lik() metode:
- indre konsistens: verdien av hashCode () kan bare endres hvis en eiendom som er i er lik() Endringer
- er lik konsistens: objekter som er like hverandre, må returnere samme hashCode
- kollisjoner: ulike objekter kan ha samme hashCode
3.2. Krenker konsistensen av hashCode () og er lik()
Det andre kriteriet i hashCode-metodekontrakten har en viktig konsekvens: Hvis vi overstyrer lik (), må vi også overstyre hashCode (). Og dette er uten tvil det mest utbredte bruddet på kontraktene til er lik() og hashCode () metoder.
La oss se et slikt eksempel:
klasse Team {Strengby; Strengavdeling; @Override public final boolean equals (Object o) {// implementering}}
De Team klasse tilsidesetter bare er lik(), men den bruker fortsatt implisitt standardimplementeringen av hashCode () som definert i Gjenstand klasse. Og dette gir en annen hashCode () for hver forekomst av klassen. Dette bryter med den andre regelen.
Nå hvis vi lager to Team objekter, både med byen "New York" og avdelingen "markedsføring", vil de være like, men de vil returnere forskjellige hashCodes.
3.3. HashMap Nøkkel med en inkonsekvent hashCode ()
Men hvorfor er kontraktsbrudd i vår Team klasse et problem? Vel, problemet starter når noen hasjbaserte samlinger er involvert. La oss prøve å bruke vår Team klasse som en nøkkel til en HashMap:
Kartledere = nye HashMap (); ledere.put (nytt team ("New York", "utvikling"), "Anne"); ledere.put (nytt team ("Boston", "utvikling"), "Brian"); ledere.put (nytt team ("Boston", "markedsføring"), "Charlie"); Team myTeam = nytt team ("New York", "utvikling"); Streng myTeamLeader = ledere.get (myTeam);
Vi forventer myTeamLeader å returnere “Anne”. Men med den gjeldende koden, gjør den det ikke.
Hvis vi vil bruke forekomster av Team klasse som HashMap tastene, må vi overstyre hashCode () metode slik at den overholder kontrakten: Like objekter returnerer det samme hashCode.
La oss se et eksempel på implementering:
@ Override public final int hashCode () {int result = 17; hvis (by! = null) {resultat = 31 * resultat + by.hashCode (); } hvis (avdeling! = null) {resultat = 31 * resultat + avdeling.hashCode (); } returnere resultat; }
Etter denne endringen, ledere.get (myTeam) returnerer “Anne” som forventet.
4. Når overstyrer vi? er lik() og hashCode ()?
Generelt sett ønsker vi å overstyre begge eller ingen av dem. Vi har nettopp sett i avsnitt 3 de uønskede konsekvensene hvis vi ignorerer denne regelen.
Domenedrevet design kan hjelpe oss med å bestemme omstendighetene når vi skal la dem være. For enhetsklasser - for objekter som har en egen identitet - er standardimplementeringen ofte fornuftig.
Derimot, for verdiobjekter foretrekker vi vanligvis likhet basert på deres egenskaper. Dermed vil overstyre er lik() og hashCode (). Husk vår Penger klasse fra seksjon 2: 55 USD tilsvarer 55 USD - selv om de er to separate forekomster.
5. Implementeringshjelpere
Vi skriver vanligvis ikke implementeringen av disse metodene for hånd. Som man kan se, er det ganske mange fallgruver.
En vanlig måte er å la IDE generere er lik() og hashCode () metoder.
Apache Commons Lang og Google Guava har hjelpeklasser for å forenkle skrivingen av begge metodene.
Prosjekt Lombok gir også en @EqualsAndHashCode kommentar. Merk igjen hvordan er lik() og hashCode () "Gå sammen" og til og med ha en felles kommentar.
6. Verifisering av kontraktene
Hvis vi ønsker å sjekke om implementeringene våre overholder Java SE-kontraktene og også noen gode fremgangsmåter, vi kan bruke EqualsVerifier-biblioteket.
La oss legge til EqualsVerifier Maven testavhengighet:
nl.jqno.equalsverifier equalsverifier 3.0.3 test
La oss bekrefte at vår Team klassen følger er lik() og hashCode () kontrakter:
@Test offentlig ugyldighet er likHashCodeContracts () {EqualsVerifier.forClass (Team.class) .verify (); }
Det er verdt å merke seg det EqualsVerifier tester både er lik() og hashCode () metoder.
EqualsVerifier er mye strengere enn Java SE-kontrakten. For eksempel sørger det for at metodene våre ikke kan kaste et NullPointerException. Det håndhever også at begge metodene, eller selve klassen, er endelige.
Det er viktig å innse det standardkonfigurasjonen av EqualsVerifier tillater bare uforanderlige felt. Dette er en strengere sjekk enn hva Java SE-kontrakten tillater. Dette følger en anbefaling fra Domain-Driven Design for å gjøre verdiobjekter uforanderlige.
Hvis vi finner noen av de innebygde begrensningene unødvendige, kan vi legge til en undertrykk (Advarsel.SPECIFIC_WARNING) til vår EqualsVerifier anrop.
7. Konklusjon
I denne artikkelen har vi diskutert er lik() og hashCode () kontrakter. Vi bør huske å:
- Overstyr alltid hashCode () hvis vi overstyrer er lik()
- Overstyring er lik() og hashCode () for verdiobjekter
- Vær oppmerksom på feller for å utvide klasser som har overstyrt er lik() og hashCode ()
- Vurder å bruke en IDE eller et tredjepartsbibliotek for å generere er lik() og hashCode () metoder
- Vurder å bruke EqualsVerifier for å teste implementeringen
Til slutt kan alle kodeeksempler finnes på GitHub.