HashCode Java de base et démonstrations equals

J'aime souvent utiliser ce blog pour revisiter des leçons durement gagnées sur les bases de Java. Ce billet de blog en est un exemple et se concentre sur l'illustration de la puissance dangereuse derrière les méthodes equals (Object) et hashCode (). Je ne couvrirai pas toutes les nuances de ces deux méthodes très importantes que tous les objets Java ont, qu'elles soient explicitement déclarées ou implicitement héritées d'un parent (éventuellement directement de Object lui-même), mais je couvrirai certains des problèmes courants qui surviennent lorsque ceux-ci sont non implémentées ou ne sont pas implémentées correctement. J'essaie également de montrer par ces démonstrations pourquoi il est important de procéder à des révisions minutieuses du code, à des tests unitaires approfondis et / ou à une analyse basée sur des outils pour vérifier l'exactitude des implémentations de ces méthodes.

Comme tous les objets Java héritent finalement des implémentations pour equals(Object)et hashCode(), le compilateur Java et en fait le lanceur d'exécution Java ne rapporteront aucun problème lors de l'appel de ces «implémentations par défaut» de ces méthodes. Malheureusement, lorsque ces méthodes sont nécessaires, les implémentations par défaut de ces méthodes (comme leur cousine la méthode toString) sont rarement ce que l'on souhaite. La documentation de l' API basée Javadoc-pour la classe Object discute du « contrat » attendu d'une mise en œuvre des equals(Object)et hashCode()méthodes et examine également la mise en œuvre par défaut probable de chaque cas de remplacement par les classes d'enfants.

Pour les exemples de cet article, j'utiliserai la classe HashAndEquals dont la liste de code est affichée à côté des instanciations d'objet de processus de diverses classes Person avec différents niveaux de prise en charge hashCodeet equalsméthodes.

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 ci-dessus sera utilisée telle quelle à plusieurs reprises avec un seul changement mineur plus tard dans l'article. Cependant, la Personclasse sera modifiée pour refléter l'importance de equalset hashCodeet pour démontrer à quel point il peut être facile de les gâcher tout en étant en même temps difficile de localiser le problème en cas d'erreur.

Pas d'explicite equalsou de hashCodeméthodes

La première version de la Personclasse ne fournit pas de version explicite remplacée de la equalsméthode ou de la hashCodeméthode. Cela démontrera «l'implémentation par défaut» de chacune de ces méthodes héritée de Object. Voici le code source pour Personsans hashCodeou equalsexplicitement remplacé.

Person.java (pas de méthode hashCode ou equals explicite)

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; } } 

Cette première version de Personne fournit pas de méthodes get / set et ne fournit equalsni hashCodeimplémentations. Lorsque la classe de démonstration principale HashAndEqualsest exécutée avec des instances de cette classe equals-less et hashCode-less Person, les résultats apparaissent comme indiqué dans la capture d'écran suivante.

Plusieurs observations peuvent être faites à partir de la sortie ci-dessus. Premièrement, sans implémentation explicite d'une equals(Object)méthode, aucune des instances de Personn'est considérée comme égale, même lorsque tous les attributs des instances (les deux chaînes) sont identiques. En effet, comme expliqué dans la documentation d'Object.equals (Object), l' equalsimplémentation par défaut est basée sur une correspondance de référence exacte:

La méthode equals pour la classe Object implémente la relation d'équivalence la plus discriminante possible sur les objets; c'est-à-dire que pour toutes les valeurs de référence non nulles x et y, cette méthode renvoie true si et seulement si x et y font référence au même objet (x == y a la valeur true).

Une deuxième observation de ce premier exemple est que le code de hachage est différent pour chaque instance de l' Personobjet, même lorsque deux instances partagent les mêmes valeurs pour tous leurs attributs. Le HashSet retourne truelorsqu'un objet "unique" est ajouté (HashSet.add) à l'ensemble ou falsesi l'objet ajouté n'est pas considéré comme unique et n'est donc pas ajouté. De même, la HashSetméthode de suppression de s renvoie truesi l 'objet fourni est considéré comme trouvé et supprimé ou falsesi l' objet spécifié est considéré comme ne faisant pas partie de HashSetet ne peut donc pas être supprimé. Étant donné que les méthodes par défaut equalset hashCodehéritées traitent ces instances comme complètement différentes, il n'est pas surprenant que toutes soient ajoutées à l'ensemble et que toutes soient supprimées avec succès de l'ensemble.

equalsMéthode explicite uniquement

La deuxième version de la Personclasse comprend une equalsméthode explicitement remplacée , comme indiqué dans la liste de code suivante.

Person.java (méthode explicite égale fournie)

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; } } 

Lorsque des instances de ceci Personavec equals(Object)une définition explicite sont utilisées, la sortie est comme indiqué dans la capture d'écran suivante.

La première observation est que maintenant, les equalsappels sur les Personinstances retournent effectivement truelorsque l'objet est égal en termes de tous les attributs étant les mêmes plutôt que de vérifier une égalité de référence stricte. Cela démontre que l' equalsimplémentation personnalisée sur Persona fait son travail. La deuxième observation est que la mise en œuvre de la equalsméthode n'a eu aucun effet sur la capacité d'ajouter et de supprimer le même objet apparemment dans le HashSet.

Explicite equalset hashCodeméthodes

Il est maintenant temps d'ajouter une hashCode()méthode explicite à la Personclasse. En effet, cela aurait vraiment dû être fait lorsque la equalsméthode a été implémentée. La raison en est indiquée dans la documentation de la Object.equals(Object)méthode:

Notez qu'il est généralement nécessaire de remplacer la méthode hashCode chaque fois que cette méthode est surchargée, afin de conserver le contrat général de la méthode hashCode, qui stipule que les objets égaux doivent avoir des codes de hachage égaux.

Voici Personune hashCodeméthode implémentée explicitement basée sur les mêmes attributs Personque la equalsméthode.

Person.java (implémentations explicites égales et 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; } } 

Le résultat de l'exécution avec la nouvelle Personclasse avec les méthodes hashCodeet equalsest affiché ci-après.

Il n'est pas surprenant que les codes de hachage retournés pour les objets avec les mêmes valeurs d'attributs soient désormais les mêmes, mais l'observation la plus intéressante est que nous ne pouvons ajouter que deux des quatre instances à l' HashSetinstant. Cela est dû au fait que les troisième et quatrième tentatives d'ajout sont considérées comme des tentatives d'ajout d'un objet qui a déjà été ajouté à l'ensemble. Comme il n'y en a eu que deux ajoutés, seuls deux peuvent être trouvés et supprimés.

Le problème avec les attributs de hashCode Mutable

Pour le quatrième et dernier exemple de cet article, je regarde ce qui se passe lorsque l' hashCodeimplémentation est basée sur un attribut qui change. Pour cet exemple, une setFirstNameméthode est ajoutée Personet le finalmodificateur est supprimé de son firstNameattribut. De plus, la classe principale HashAndEquals doit avoir le commentaire supprimé de la ligne qui appelle cette nouvelle méthode set. La nouvelle version de Persons'affiche ensuite.

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; } } 

La sortie générée lors de l'exécution de cet exemple est illustrée ci-après.