HashCode Java di base ed è uguale a Dimostrazioni

Spesso mi piace usare questo blog per rivisitare le lezioni duramente guadagnate sulle basi di Java. Questo post del blog è uno di questi esempi e si concentra sull'illustrazione del potere pericoloso dietro i metodi equals (Object) e hashCode (). Non tratterò ogni sfumatura di questi due metodi altamente significativi che tutti gli oggetti Java hanno dichiarato esplicitamente o implicitamente ereditato da un genitore (possibilmente direttamente dall'Oggetto stesso), ma tratterò alcuni dei problemi comuni che sorgono quando questi sono non implementati o non sono implementati correttamente. Cerco anche di mostrare con queste dimostrazioni perché è importante per un'attenta revisione del codice, test di unità approfonditi e / o analisi basata su strumenti per verificare la correttezza delle implementazioni di questi metodi.

Poiché tutti gli oggetti Java alla fine ereditano le implementazioni per equals(Object)e hashCode(), il compilatore Java e in effetti il ​​lanciatore di runtime Java non segnaleranno alcun problema quando invocano queste "implementazioni predefinite" di questi metodi. Sfortunatamente, quando questi metodi sono necessari, le implementazioni predefinite di questi metodi (come il loro cugino, il metodo toString) sono raramente ciò che si desidera. La documentazione API Javadoc-based per la classe Object discute il "contratto" atteso di qualsiasi implementazione della equals(Object)e hashCode()metodi e discute anche l'implementazione di default di ogni probabile se non vengono escluse dalle classi figlie.

Per gli esempi in questo post, userò la classe HashAndEquals il cui elenco di codice è mostrato accanto per elaborare istanze di oggetti di varie classi Person con diversi livelli di supporto per hashCodee equalsmetodi.

HashAndEquals.java

package dustin.examples; import java.util.HashSet; import java.util.Set; import static java.lang.System.out; public class HashAndEquals { private static final String HEADER_SEPARATOR = "======================================================================"; private static final int HEADER_SEPARATOR_LENGTH = HEADER_SEPARATOR.length(); private static final String NEW_LINE = System.getProperty("line.separator"); private final Person person1 = new Person("Flintstone", "Fred"); private final Person person2 = new Person("Rubble", "Barney"); private final Person person3 = new Person("Flintstone", "Fred"); private final Person person4 = new Person("Rubble", "Barney"); public void displayContents() { printHeader("THE CONTENTS OF THE OBJECTS"); out.println("Person 1: " + person1); out.println("Person 2: " + person2); out.println("Person 3: " + person3); out.println("Person 4: " + person4); } public void compareEquality() { printHeader("EQUALITY COMPARISONS"); 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("ADD ELEMENTS TO SET - ARE THEY ADDED OR THE SAME?"); final Set set = new 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)); return set; } public void removeFromHashSet(final Set sourceSet) { printHeader("REMOVE ELEMENTS FROM SET - CAN THEY BE FOUND TO BE REMOVED?"); 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[] arguments) { final HashAndEquals instance = new HashAndEquals(); instance.displayContents(); instance.compareEquality(); instance.compareHashCodes(); final Set set = instance.addToHashSet(); out.println("Set Before Removals: " + set); //instance.person1.setFirstName("Bam Bam"); instance.removeFromHashSet(set); out.println("Set After Removals: " + set); } } 

La classe di cui sopra verrà usata ripetutamente così com'è con solo una piccola modifica più avanti nel post. Tuttavia, la Personclasse verrà cambiata per riflettere l'importanza di equalse hashCodee per dimostrare quanto sia facile rovinarli e allo stesso tempo essere difficile rintracciare il problema quando si verifica un errore.

Nessun metodo equalso esplicitohashCode

La prima versione della Personclasse non fornisce una versione con sovrascrittura esplicita del equalsmetodo o del hashCodemetodo. Ciò dimostrerà l '"implementazione predefinita" di ciascuno di questi metodi ereditati Object. Ecco il codice sorgente per Personsenza hashCodeo equalsesplicitamente sovrascritto.

Person.java (nessun hashCode esplicito o metodo uguale)

package dustin.examples; public class Person { private final String lastName; private final String firstName; public Person(final String newLastName, final String newFirstName) { this.lastName = newLastName; this.firstName = newFirstName; } @Override public String toString() { return this.firstName + " " + this.lastName; } } 

Questa prima versione di Personnon fornisce metodi get / set e non fornisce equalshashCodeimplementazioni. Quando la classe dimostrativa principale HashAndEqualsviene eseguita con istanze di questa classe equals-less e hashCode-less Person, i risultati vengono visualizzati come mostrato nell'istantanea della schermata successiva.

Si possono fare diverse osservazioni dall'output mostrato sopra. Primo, senza l'implementazione esplicita di un equals(Object)metodo, nessuna delle istanze di Personè considerata uguale, anche quando tutti gli attributi delle istanze (le due stringhe) sono identiche. Questo perché, come spiegato nella documentazione per Object.equals (Object), l' equalsimplementazione predefinita si basa su una corrispondenza di riferimento esatta:

Il metodo equals per la classe Object implementa la relazione di equivalenza più discriminante possibile sugli oggetti; ovvero, per qualsiasi valore di riferimento non nullo x e y, questo metodo restituisce vero se e solo se x e y si riferiscono allo stesso oggetto (x == y ha il valore vero).

Una seconda osservazione da questo primo esempio è che il codice hash è diverso per ogni istanza Persondell'oggetto anche quando due istanze condividono gli stessi valori per tutti i loro attributi. HashSet ritorna truequando un oggetto "unico" viene aggiunto (HashSet.add) al set o falsese l'oggetto aggiunto non è considerato unico e quindi non viene aggiunto. Allo stesso modo, il HashSetmetodo remove restituisce truese l'oggetto fornito è considerato trovato e rimosso o falsese l'oggetto specificato non è considerato parte del HashSete quindi non può essere rimosso. Poiché i metodi predefiniti equalse hashCodeereditati trattano queste istanze come completamente diverse, non sorprende che vengano tutte aggiunte al set e tutte rimosse con successo dal set.

equalsSolo metodo esplicito

La seconda versione della Personclasse include un equalsmetodo sovrascritto in modo esplicito , come mostrato nel listato di codice successivo.

Person.java (metodo di uguale esplicito fornito)

package dustin.examples; public class Person { private final String lastName; private final String firstName; public Person(final String newLastName, final String newFirstName) { this.lastName = newLastName; this.firstName = newFirstName; } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (this == obj) { return true; } if (this.getClass() != obj.getClass()) { return false; } final Person other = (Person) obj; if (this.lastName == null ? other.lastName != null : !this.lastName.equals(other.lastName)) { return false; } if (this.firstName == null ? other.firstName != null : !this.firstName.equals(other.firstName)) { return false; } return true; } @Override public String toString() { return this.firstName + " " + this.lastName; } } 

Quando vengono utilizzate istanze di this Personcon equals(Object)una definizione esplicita, l'output è come mostrato nell'istantanea della schermata successiva.

La prima osservazione è che ora le equalschiamate alle Personistanze ritornano effettivamente truequando l'oggetto è uguale in termini di tutti gli attributi che sono gli stessi piuttosto che verificare una rigorosa uguaglianza di riferimento. Ciò dimostra che l' equalsimplementazione personalizzata su Personha fatto il suo lavoro. La seconda osservazione è che l'implementazione del equalsmetodo non ha avuto alcun effetto sulla capacità di aggiungere e rimuovere lo stesso oggetto apparentemente dal file HashSet.

Esplicito equalse hashCodemetodi

È ora il momento di aggiungere un hashCode()metodo esplicito alla Personclasse. In effetti, questo avrebbe dovuto essere fatto quando il equalsmetodo è stato implementato. La ragione di ciò è indicata nella documentazione del Object.equals(Object)metodo:

Si noti che è generalmente necessario sovrascrivere il metodo hashCode ogni volta che questo metodo viene sovrascritto, in modo da mantenere il contratto generale per il metodo hashCode, che afferma che oggetti uguali devono avere codici hash uguali.

Qui è Personun esplicito implementata hashCodemetodo basato sugli stessi attributi Personcome il equalsmetodo.

Person.java (uguale esplicito e implementazioni hashCode)

package dustin.examples; public class Person { private final String lastName; private final String firstName; public Person(final String newLastName, final String newFirstName) { this.lastName = newLastName; this.firstName = newFirstName; } @Override public int hashCode() { return lastName.hashCode() + firstName.hashCode(); } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (this == obj) { return true; } if (this.getClass() != obj.getClass()) { return false; } final Person other = (Person) obj; if (this.lastName == null ? other.lastName != null : !this.lastName.equals(other.lastName)) { return false; } if (this.firstName == null ? other.firstName != null : !this.firstName.equals(other.firstName)) { return false; } return true; } @Override public String toString() { return this.firstName + " " + this.lastName; } } 

Di seguito viene mostrato l' output dell'esecuzione con la nuova Personclasse con i metodi hashCodee equals.

Non sorprende che i codici hash restituiti per oggetti con gli stessi valori di attributi ora siano gli stessi, ma l'osservazione più interessante è che possiamo aggiungere solo due delle quattro istanze a HashSetnow. Questo perché si considera che il terzo e il quarto tentativo di aggiunta tentino di aggiungere un oggetto che era già stato aggiunto al set. Poiché sono stati aggiunti solo due, solo due possono essere trovati e rimossi.

Il problema con gli attributi hashCode mutevoli

Per il quarto e ultimo esempio in questo post, guardo cosa succede quando l' hashCodeimplementazione si basa su un attributo che cambia. In questo esempio, setFirstNameviene aggiunto un metodo Persone il finalmodificatore viene rimosso dal suo firstNameattributo. Inoltre, la classe principale HashAndEquals deve rimuovere il commento dalla riga che richiama questo nuovo metodo set. Di seguito Personviene mostrata la nuova versione di .

package dustin.examples; public class Person { private final String lastName; private String firstName; public 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 public boolean equals(Object obj) { if (obj == null) { return false; } if (this == obj) { return true; } if (this.getClass() != obj.getClass()) { return false; } final Person other = (Person) obj; if (this.lastName == null ? other.lastName != null : !this.lastName.equals(other.lastName)) { return false; } if (this.firstName == null ? other.firstName != null : !this.firstName.equals(other.firstName)) { return false; } return true; } @Override public String toString() { return this.firstName + " " + this.lastName; } } 

L'output generato dall'esecuzione di questo esempio è mostrato di seguito.