Programmering

Grunnleggende Java hashCode og tilsvarer demonstrasjoner

Jeg liker ofte å bruke denne bloggen til å se på hardt opptjente leksjoner i det grunnleggende om Java. Dette blogginnlegget er et slikt eksempel og fokuserer på illustrasjon av den farlige kraften bak lik (Object) og hashCode () -metodene. Jeg vil ikke dekke alle nyanser av disse to svært viktige metodene som alle Java-objekter har, enten de er eksplisitt erklært eller implisitt arvet fra en forelder (muligens direkte fra selve objektet), men jeg vil dekke noen av de vanlige problemene som oppstår når disse er ikke implementert eller er ikke implementert riktig. Jeg prøver også å vise ved disse demonstrasjonene hvorfor det er viktig for nøye kodegjennomgang, grundig enhetstesting og / eller verktøybasert analyse for å verifisere riktigheten av implementeringen av disse metodene.

Fordi alle Java-objekter til slutt arver implementeringer for er lik (Objekt) og hashCode (), vil Java-kompilatoren og faktisk Java runtime launcher ikke rapportere noe problem når de påkaller disse "standardimplementeringene" av disse metodene. Dessverre, når disse metodene er nødvendige, er standardimplementeringene av disse metodene (som fetteren deres toString-metoden) sjelden det som er ønsket. Den Javadoc-baserte API-dokumentasjonen for Object-klassen diskuterer "kontrakten" som forventes av implementering av er lik (Objekt) og hashCode () metoder og drøfter også den sannsynlige standardimplementeringen av hver hvis ikke overstyrt av barneklasser.

For eksemplene i dette innlegget bruker jeg HashAndEquals-klassen hvis kodeliste vises ved siden av prosessobjektinstanseringer av forskjellige personklasser med forskjellige nivåer av støtte for hashCode og er lik metoder.

HashAndEquals.java

pakke dustin. eksempler; importere java.util.HashSet; importere java.util.Set; importere statisk java.lang.System.out; offentlig klasse HashAndEquals {privat statisk finale String HEADER_SEPARATOR = "=========================================== ================================= "; privat statisk slutt int HEADER_SEPARATOR_LENGTH = HEADER_SEPARATOR.length (); privat statisk sluttstreng NEW_LINE = System.getProperty ("line.separator"); privat slutt Person person1 = ny person ("Flintstone", "Fred"); privat slutt Person person2 = ny person ("Rubble", "Barney"); privat final Person person3 = ny person ("Flintstone", "Fred"); privat sluttperson Person person4 = ny person ("Rubble", "Barney"); public void displayContents () {printHeader ("INNHOLDET I MÅLENE"); out.println ("Person 1:" + person1); out.println ("Person 2:" + person2); out.println ("Person 3:" + person3); out.println ("Person 4:" + person4); } offentlig ugyldighet CompareEquality () {printHeader ("LIKESTILLINGSSAMMENLIKNINGER"); out.println ("Person1.equals (Person2):" + person1.equals (person2)); out.println ("Person1.equals (Person3):" + person1.equals (person3)); out.println ("Person2.equals (Person4):" + person2.equals (person4)); } public void CompareHashCodes () {printHeader ("COMPARE HASH CODES"); out.println ("Person1.hashCode ():" + person1.hashCode ()); out.println ("Person2.hashCode ():" + person2.hashCode ()); out.println ("Person3.hashCode ():" + person3.hashCode ()); out.println ("Person4.hashCode ():" + person4.hashCode ()); } public Set addToHashSet () {printHeader ("LEGG TIL ELEMENTER Å STILLE - ER DE LAGT ELLER Samme?"); endelig sett sett = nytt HashSet (); out.println ("Set.add (Person1):" + set.add (person1)); out.println ("Set.add (Person2):" + set.add (person2)); out.println ("Set.add (Person3):" + set.add (person3)); out.println ("Set.add (Person4):" + set.add (person4)); retur sett; } public void removeFromHashSet (final Set sourceSet) {printHeader ("FJERN ELEMENTER FRA SET - KAN DE FINNES FOR Å FJERNES?"); out.println ("Set.remove (Person1):" + sourceSet.remove (person1)); out.println ("Set.remove (Person2):" + sourceSet.remove (person2)); out.println ("Set.remove (Person3):" + sourceSet.remove (person3)); out.println ("Set.remove (Person4):" + sourceSet.remove (person4)); } public static void printHeader (final String headerText) {out.println (NEW_LINE); out.println (HEADER_SEPARATOR); out.println ("=" + headerText); out.println (HEADER_SEPARATOR); } public static void main (final String [] argumenter) {final HashAndEquals instance = new HashAndEquals (); instans.displayContents (); instance.compareEquality (); instance.compareHashCodes (); final Set set = instance.addToHashSet (); out.println ("Sett før fjerning:" + sett); //instance.person1.setFirstName("Bam Bam "); instance.removeFromHashSet (sett); out.println ("Sett etter fjerning:" + sett); }} 

Klassen ovenfor vil bli brukt som den er gjentatte ganger med bare en mindre endring senere i innlegget. Imidlertid, den Person klasse vil bli endret for å gjenspeile viktigheten av er lik og hashCode og for å demonstrere hvor lett det kan være å rote opp disse samtidig som det er vanskelig å spore opp problemet når det er feil.

Ingen eksplisitt er lik eller hashCode Metoder

Den første versjonen av Person klasse gir ikke en eksplisitt overstyrt versjon av verken er lik metoden eller hashCode metode. Dette vil demonstrere "standardimplementeringen" av hver av disse metodene arvet fra Gjenstand. Her er kildekoden for Person uten hashCode eller er lik eksplisitt overstyrt.

Person.java (ingen eksplisitt hashCode eller lik metode)

pakke dustin. eksempler; offentlig klasse Person {privat finale String etternavn; privat finale Streng fornavn; offentlig person (final String newLastName, final String newFirstName) {this.lastName = newLastName; this.firstName = newFirstName; } @ Override public String toString () {return this.firstName + "" + this.lastName; }} 

Denne første versjonen av Person gir ikke get / set-metoder og gir ikke er lik eller hashCode implementeringer. Når hoveddemonstrasjonsklassen HashAndEquals utføres med forekomster av dette er lik-løs og hashCode-mindre Person klasse vises resultatene som vist i neste skjermbilde.

Flere observasjoner kan gjøres fra utdataene vist ovenfor. Først uten eksplisitt implementering av en er lik (Objekt) metode, ingen av forekomster av Person regnes som like, selv når alle attributtene til instansene (de to strengene) er identiske. Dette er, som forklart i dokumentasjonen for Object.equals (Object), standard er lik implementering er basert på en nøyaktig referansetreff:

Likemetoden for klasse Objekt implementerer den mest diskriminerende mulige ekvivalensrelasjonen på objekter; det vil si for alle ikke-null referanseverdier x og y, denne metoden returnerer true hvis og bare hvis x og y refererer til det samme objektet (x == y har verdien true).

En annen observasjon fra dette første eksemplet er at hash-koden er forskjellig for hver forekomst av Person motsette seg selv når to forekomster deler de samme verdiene for alle attributtene. HashSet returnerer ekte når et "unikt" objekt legges til (HashSet.add) i settet eller falsk hvis objektet som legges til, ikke betraktes som unikt og ikke blir lagt til Tilsvarende HashSetFjern metoden returnerer ekte hvis det medfølgende objektet anses å være funnet og fjernet eller falsk hvis det spesifiserte objektet anses å ikke være en del av HashSet og så kan ikke fjernes. Fordi det er lik og hashCode arvede standardmetoder behandler disse forekomster som helt forskjellige, det er ingen overraskelse at alle blir lagt til settet, og alle er vellykket fjernet fra settet.

Eksplisitt er lik Bare metoden

Den andre versjonen av Person klasse inkluderer en eksplisitt overstyrt er lik metode som vist i neste kodeliste.

Person.java (eksplisitt lik metode gitt)

pakke dustin. eksempler; offentlig klasse Person {privat finale String etternavn; privat finale Streng fornavn; offentlig person (final String newLastName, final String newFirstName) {this.lastName = newLastName; this.firstName = newFirstName; } @Override offentlig boolsk er lik (Objekt obj) {if (obj == null) {return false; } hvis (dette == obj) {return true; } hvis (this.getClass ()! = obj.getClass ()) {return false; } endelig person annen = (person) obj; if (this.lastName == null? other.lastName! = null:! this.lastName.equals (other.lastName)) {return false; } hvis (this.firstName == null? other.firstName! = null:! this.firstName.equals (other.firstName)) {return false; } returner sant; } @ Override public String toString () {return this.firstName + "" + this.lastName; }} 

Når forekomster av dette Person med er lik (Objekt) eksplisitt definerte brukes, blir utdataene som vist i neste skjermbilde.

Den første observasjonen er at nå er lik ringer på Person tilfeller kommer faktisk tilbake ekte når objektet er likt med tanke på at alle attributter er like i stedet for å sjekke for en streng referanselikhet. Dette demonstrerer at skikken er lik implementering på Person har gjort jobben sin. Den andre observasjonen er at implementering av er lik metoden har ikke hatt noen innvirkning på muligheten til å legge til og fjerne det tilsynelatende samme objektet til HashSet.

Eksplisitt er lik og hashCode Metoder

Det er nå på tide å legge til et eksplisitt hashCode () metoden til Person klasse. Dette burde egentlig ha blitt gjort når er lik metoden ble implementert. Årsaken til dette er angitt i dokumentasjonen for Object.equals (Object) metode:

Merk at det generelt er nødvendig å overstyre hashCode-metoden når denne metoden blir overstyrt, for å opprettholde den generelle kontrakten for hashCode-metoden, som sier at like objekter må ha like hash-koder.

Her er Person med en eksplisitt implementert hashCode metode basert på de samme attributtene til Person som er lik metode.

Person.java (eksplisitt lik og hashCode-implementeringer)

pakke dustin. eksempler; offentlig klasse Person {privat finale String etternavn; privat finale Streng fornavn; offentlig person (final String newLastName, final String newFirstName) {this.lastName = newLastName; this.firstName = newFirstName; } @ Override public int hashCode () {return lastName.hashCode () + firstName.hashCode (); } @Override offentlig boolsk er lik (Objekt obj) {if (obj == null) {return false; } hvis (dette == obj) {return true; } hvis (this.getClass ()! = obj.getClass ()) {return false; } endelig person annen = (person) obj; if (this.lastName == null? other.lastName! = null:! this.lastName.equals (other.lastName)) {return false; } hvis (this.firstName == null? other.firstName! = null:! this.firstName.equals (other.firstName)) {return false; } returner sant; } @ Override public String toString () {return this.firstName + "" + this.lastName; }} 

Resultatet fra å kjøre med det nye Person klasse med hashCode og er lik metoder vises neste.

Det er ikke overraskende at hash-kodene som returneres for objekter med de samme attributtenes verdier nå er de samme, men jo mer interessant er det at vi bare kan legge til to av de fire forekomster til HashSet nå. Dette er fordi det tredje og fjerde tilsetningsforsøket anses å forsøke å legge til et objekt som allerede var lagt til settet. Fordi det bare var to lagt til, kan bare to bli funnet og fjernet.

Trouble with Mutable hashCode Attributter

For det fjerde og siste eksemplet i dette innlegget ser jeg på hva som skjer når hashCode implementering er basert på et attributt som endres. For dette eksemplet, a setFirstName metoden er lagt til Person og endelig modifikator fjernes fra fornavn Egenskap. I tillegg må HashAndEquals-klassen få kommentaren fjernet fra linjen som påkaller denne nye settmetoden. Den nye versjonen av Person vises neste.

pakke dustin. eksempler; offentlig klasse Person {privat finale String etternavn; privat streng fornavn; offentlig person (final String newLastName, final String newFirstName) {this.lastName = newLastName; this.firstName = newFirstName; } @Override public int hashCode () {return lastName.hashCode () + firstName.hashCode (); } public void setFirstName (final String newFirstName) {this.firstName = newFirstName; } @Override offentlig boolsk er lik (Objekt obj) {if (obj == null) {return false; } hvis (dette == obj) {return true; } hvis (this.getClass ()! = obj.getClass ()) {return false; } endelig person annen = (person) obj; if (this.lastName == null? other.lastName! = null:! this.lastName.equals (other.lastName)) {return false; } hvis (this.firstName == null? other.firstName! = null:! this.firstName.equals (other.firstName)) {return false; } returner sant; } @ Override public String toString () {return this.firstName + "" + this.lastName; }} 

Utgang generert fra å kjøre dette eksemplet vises neste.

$config[zx-auto] not found$config[zx-overlay] not found