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:
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:
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.