Programmering

Grundlæggende Java hashCode og svarer til demonstrationer

Jeg kan ofte lide at bruge denne blog til at revidere hårdt tjente lektioner i det grundlæggende i Java. Dette blogindlæg er et sådant eksempel og fokuserer på illustration af den farlige magt bag ligemetoden (Object) og hashCode (). Jeg vil ikke dække alle nuancer af disse to meget betydningsfulde metoder, som alle Java-objekter har, hvad enten de er eksplicit erklæret eller implicit arvet fra en forælder (muligvis direkte fra selve objektet), men jeg vil dække nogle af de almindelige problemer, der opstår, når disse er ikke implementeret eller er ikke implementeret korrekt. Jeg forsøger også ved disse demonstrationer at vise, hvorfor det er vigtigt for omhyggelig kodevurdering, grundig enhedstest og / eller værktøjsbaseret analyse for at kontrollere rigtigheden af ​​disse metoders implementeringer.

Fordi alle Java-objekter i sidste ende arver implementeringer til er lig med (Objekt) og hashCode (), Java-kompilatoren og faktisk Java runtime launcher rapporterer ikke noget problem, når de påberåber sig disse "standardimplementeringer" af disse metoder. Desværre, når disse metoder er nødvendige, er standardimplementeringer af disse metoder (som deres fætter toString-metoden) sjældent, hvad der ønskes. Den Javadoc-baserede API-dokumentation til objektklassen diskuterer den "kontrakt", der forventes af enhver implementering af er lig med (Objekt) og hashCode () metoder og diskuterer også den sandsynlige standardimplementering af hver, hvis ikke tilsidesat af barneklasser.

For eksemplerne i dette indlæg bruger jeg HashAndEquals-klassen, hvis kodeliste vises ved siden af ​​procesinstantiering af forskellige personklasser med forskellige niveauer af support til hashCode og lige med 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 endelig streng HEADER_SEPARATOR = "=========================================== ================================= "; privat statisk endelig int HEADER_SEPARATOR_LENGTH = HEADER_SEPARATOR.length (); privat statisk endelig streng NEW_LINE = System.getProperty ("line.separator"); privat endelig Person person1 = ny person ("Flintstone", "Fred"); privat endelig Person person2 = ny person ("Rubble", "Barney"); privat endelig Person person3 = ny person ("Flintstone", "Fred"); privat endelig Person person4 = ny person ("Rubble", "Barney"); public void displayContents () {printHeader ("FORMÅLETS INDHOLD"); out.println ("Person 1:" + person1); out.println ("Person 2:" + person2); out.println ("Person 3:" + person3); out.println ("Person 4:" + person4); } offentlig ugyldighed CompareEquality () {printHeader ("LIGESTILLINGSSAMPARATIONER"); out.println ("Person1.equals (Person2):" + person1.equals (person2)); out.println ("Person1.equals (Person3):" + person1.equals (person3)); out.println ("Person2.equals (Person4):" + person2.equals (person4)); } offentlig ugyldighed 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 ("TILFØJ ELEMENTER, DER SKAL INDSTILLES - ER DE TILFØJT ELLER SELV?"); sidste sæt sæt = nyt 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 sæt; } public void removeFromHashSet (final Set sourceSet) {printHeader ("FJERN ELEMENTER FRA SÆT - KAN DE FINDES FOR AT 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)); } offentlig statisk ugyldig printHeader (endelig streng headerText) {out.println (NEW_LINE); out.println (HEADER_SEPARATOR); out.println ("=" + headerText); out.println (HEADER_SEPARATOR); } offentlig statisk ugyldig hoved (sidste String [] argumenter) {final HashAndEquals instans = ny HashAndEquals (); instans.displayContents (); instance.compareEquality (); instance.compareHashCodes (); sidste sæt sæt = instans.addToHashSet (); out.println ("Set Before Removals:" + set); //instance.person1.setFirstName("Bam Bam "); instance.removeFromHashSet (sæt); out.println ("Sæt efter fjernelse:" + sæt); }} 

Klassen ovenfor vil blive brugt som den er gentagne gange med kun en mindre ændring senere i posten. Men den Person klasse vil blive ændret for at afspejle vigtigheden af lige med og hashCode og for at demonstrere, hvor let det kan være at ødelægge disse, samtidig med at det er vanskeligt at spore problemet, når der er en fejl.

Ingen eksplicit lige med eller hashCode Metoder

Den første version af Person klasse giver ikke en eksplicit tilsidesat version af hverken lige med metode eller hashCode metode. Dette vil demonstrere "standardimplementeringen" af hver af disse metoder arvet fra Objekt. Her er kildekoden til Person uden hashCode eller lige med udtrykkeligt tilsidesat.

Person.java (ingen eksplicit hashCode eller lig metode)

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

Denne første version af Person giver ikke get / set-metoder og giver ikke lige med eller hashCode implementeringer. Når den vigtigste demonstrationsklasse HashAndEquals udføres med forekomster af dette lige med-mindre og hashCode-mindre Person klasse vises resultaterne som vist i næste skærmbillede.

Flere observationer kan foretages fra output vist ovenfor. For det første uden eksplicit implementering af en er lig med (Objekt) metode, ingen af ​​forekomsterne af Person betragtes som lige, selv når alle attributter for instanserne (de to strenge) er identiske. Dette skyldes, som det er forklaret i dokumentationen til Object.equals (Object), standard lige med implementering er baseret på en nøjagtig referencematch:

Ligemetoden til klasse Objekt implementerer den mest diskriminerende mulige ækvivalensrelation på objekter; det vil sige for alle ikke-nul-referenceværdier x og y, denne metode returnerer sand, hvis og kun hvis x og y henviser til det samme objekt (x == y har værdien sand).

En anden observation fra dette første eksempel er, at hash-koden er forskellig for hver forekomst af Person gør indsigelse, selv når to forekomster deler de samme værdier for alle deres attributter. HashSet vender tilbage rigtigt når et "unikt" objekt føjes (HashSet.add) til sættet eller falsk hvis det tilføjede objekt ikke betragtes som unikt, og så ikke tilføjes. Tilsvarende er HashSet's Fjern metode returnerer rigtigt hvis det medfølgende objekt betragtes som fundet og fjernet eller falsk hvis det angivne objekt anses for ikke at være en del af HashSet og kan derfor ikke fjernes. Fordi lige med og hashCode arvede standardmetoder behandler disse forekomster som helt forskellige, det er ingen overraskelse, at alle føjes til sættet, og alle fjernes med succes fra sættet.

Eksplicit lige med Kun metode

Den anden version af Person klasse inkluderer en eksplicit tilsidesat lige med metode som vist i næste kodeliste.

Person.java (eksplicit lig med metoden leveret)

pakke dustin. eksempler; offentlig klasse Person {privat endelig Streng efternavn; privat final String firstName; offentlig person (final String newLastName, final String newFirstName) {this.lastName = newLastName; this.firstName = newFirstName; } @Override offentlige boolske lig (Objekt obj) {hvis (obj == null) {returner false; } hvis (dette == obj) {returner sandt; } hvis (this.getClass ()! = obj.getClass ()) {returner false; } sidste Person anden = (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 sandt } @ Override public String toString () {returner this.firstName + "" + this.lastName; }} 

Når tilfælde af dette Person med er lig med (Objekt) eksplicit definerede bruges, er output som vist i næste skærmbillede.

Den første observation er, at nu lige med opfordrer til Person tilfælde vender faktisk tilbage rigtigt når objektet er ens med hensyn til at alle attributter er de samme snarere end at kontrollere for en streng reference ligestilling. Dette viser, at skik lige med implementering den Person har gjort sit job. Den anden observation er, at implementeringen af lige med metoden har ikke haft nogen indflydelse på evnen til at tilføje og fjerne det tilsyneladende samme objekt til HashSet.

Eksplicit lige med og hashCode Metoder

Det er nu tid til at tilføje en eksplicit hashCode () metode til Person klasse. Dette burde faktisk have været gjort, da lige med metode blev implementeret. Årsagen til dette fremgår af dokumentationen til Object.equals (Objekt) metode:

Bemærk, at det generelt er nødvendigt at tilsidesætte hashCode-metoden, når denne metode tilsidesættes, for at opretholde den generelle kontrakt for hashCode-metoden, som siger, at lige objekter skal have lige hash-koder.

Her er Person med en eksplicit implementeret hashCode metode baseret på de samme attributter af Person som lige med metode.

Person.java (eksplicit lig med hashCode-implementeringer)

pakke dustin. eksempler; offentlig klasse Person {privat endelig Streng efternavn; privat final String firstName; offentlig person (final String newLastName, final String newFirstName) {this.lastName = newLastName; this.firstName = newFirstName; } @ Override public int hashCode () {return lastName.hashCode () + firstName.hashCode (); } @Override offentlige boolske lig (Objekt obj) {hvis (obj == null) {returner false; } hvis (dette == obj) {returner sandt; } hvis (this.getClass ()! = obj.getClass ()) {returner false; } sidste Person anden = (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 sandt } @ Override public String toString () {returner this.firstName + "" + this.lastName; }} 

Resultatet fra at køre med det nye Person klasse med hashCode og lige med metoder vises derefter.

Det er ikke overraskende, at hashkoderne, der returneres for objekter med de samme attributters værdier, nu er de samme, men den mere interessante observation er, at vi kun kan tilføje to af de fire forekomster til HashSet nu. Dette skyldes, at det tredje og fjerde tilføjelsesforsøg anses for at forsøge at tilføje et objekt, der allerede blev føjet til sættet. Fordi der kun blev tilføjet to, kan kun to findes og fjernes.

Problemet med omskiftelige hashCode-attributter

For det fjerde og sidste eksempel i dette indlæg ser jeg på, hvad der sker, når hashCode implementering er baseret på en attribut, der ændres. I dette eksempel er a setFirstName metode tilføjes Person og endelig modifikator fjernes fra dens fornavn attribut. Derudover skal den vigtigste HashAndEquals-klasse få kommentaren fjernet fra den linje, der påberåber sig denne nye sætmetode. Den nye version af Person vises næste.

pakke dustin. eksempler; offentlig klasse Person {privat endelig Streng efternavn; 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 offentlige boolske lig (Objekt obj) {hvis (obj == null) {returner false; } hvis (dette == obj) {returner sandt; } hvis (this.getClass ()! = obj.getClass ()) {returner false; } sidste Person anden = (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 sandt } @ Override public String toString () {returner this.firstName + "" + this.lastName; }} 

Output genereret ved at køre dette eksempel vises derefter.