CodeGym /Blog Java /Random-FR /méthodes equals et hashCode : bonnes pratiques
Auteur
Milan Vucic
Programming Tutor at Codementor.io

méthodes equals et hashCode : bonnes pratiques

Publié dans le groupe Random-FR
Salut! Aujourd'hui, nous allons parler de deux méthodes importantes en Java : equals()et hashCode(). Ce n'est pas la première fois que nous les rencontrons : le cours CodeGym commence par une courte leçon sur equals()— lisez-la si vous l'avez oubliée ou si vous ne l'avez jamais vue... méthodes equals et hashCode : bonnes pratiques - 1Dans la leçon d'aujourd'hui, nous parlerons de ces concepts en détail. Et croyez-moi, nous avons de quoi nous parler ! Mais avant de passer au nouveau, rafraîchissons ce que nous avons déjà couvert :) Comme vous vous en souvenez, c'est généralement une mauvaise idée de comparer deux objets à l'aide de l'opérateur ==, car ==compare les références. Voici notre exemple avec des voitures d'une leçon récente :

public class Car {

   String model;
   int maxSpeed;

   public static void main(String[] args) {

       Car car1 = new Car();
       car1.model = "Ferrari";
       car1.maxSpeed = 300;

       Car car2 = new Car();
       car2.model = "Ferrari";
       car2.maxSpeed = 300;

       System.out.println(car1 == car2);
   }
}
Sortie console :

false
Il semble que nous ayons créé deux Carobjets identiques : les valeurs des champs correspondants des deux objets voiture sont les mêmes, mais le résultat de la comparaison est toujours faux. Nous connaissons déjà la raison : les références car1et car2pointent vers des adresses mémoire différentes, elles ne sont donc pas égales. Mais nous voulons toujours comparer les deux objets, pas deux références. La meilleure solution pour comparer des objets est la equals()méthode.

méthode equals()

Vous vous souviendrez peut-être que nous ne créons pas cette méthode à partir de zéro, nous la redéfinissons plutôt : la equals()méthode est définie dans la Objectclasse. Cela dit, dans sa forme habituelle, il est de peu d'utilité :

public boolean equals(Object obj) {
   return (this == obj);
}
C'est ainsi que la equals()méthode est définie dans la Objectclasse. Il s'agit encore une fois d'un comparatif de références. Pourquoi ont-ils fait ça comme ça ? Eh bien, comment les créateurs du langage savent-ils quels objets de votre programme sont considérés comme égaux et lesquels ne le sont pas ? :) C'est le point principal de la equals()méthode — le créateur d'une classe est celui qui détermine quelles caractéristiques sont utilisées lors de la vérification de l'égalité des objets de la classe. Ensuite, vous remplacez la equals()méthode dans votre classe. Si vous ne comprenez pas bien la signification de "détermine quelles caractéristiques", prenons un exemple. Voici une classe simple représentant un homme : Man.

public class Man {

   private String noseSize;
   private String eyesColor;
   private String haircut;
   private boolean scars;
   private int dnaCode;

public Man(String noseSize, String eyesColor, String haircut, boolean scars, int dnaCode) {
   this.noseSize = noseSize;
   this.eyesColor = eyesColor;
   this.haircut = haircut;
   this.scars = scars;
   this.dnaCode = dnaCode;
}

   // Getters, setters, etc.
}
Supposons que nous écrivions un programme qui doit déterminer si deux personnes sont de vrais jumeaux ou simplement des sosies. Nous avons cinq caractéristiques : la taille du nez, la couleur des yeux, la coiffure, la présence de cicatrices et les résultats des tests ADN (pour simplifier, nous représentons cela sous la forme d'un code entier). Selon vous, laquelle de ces caractéristiques permettrait à notre programme d'identifier des jumeaux identiques ? méthodes equals et hashCode : bonnes pratiques - 2Bien sûr, seul un test ADN peut apporter une garantie. Deux personnes peuvent avoir la même couleur d'yeux, la même coupe de cheveux, le même nez et même les mêmes cicatrices - il y a beaucoup de gens dans le monde, et il est impossible de garantir qu'il n'y a pas de sosies. Mais il nous faut un mécanisme fiable : seul le résultat d'un test ADN nous permettra de tirer une conclusion précise. Qu'est-ce que cela signifie pour notre equals()méthode ? Nous devons le remplacer dans leManclasse, en tenant compte des exigences de notre programme. La méthode doit comparer le int dnaCodechamp des deux objets. S'ils sont égaux, alors les objets sont égaux.

@Override
public boolean equals(Object o) {
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
Est-ce vraiment si simple ? Pas vraiment. Nous avons oublié quelque chose. Pour nos objets, nous avons identifié un seul champ pertinent pour établir l'égalité des objets : dnaCode. Imaginons maintenant que nous ayons non pas 1, mais 50 champs pertinents. Et si les 50 champs de deux objets sont égaux, alors les objets sont égaux. Un tel scénario est également possible. Le principal problème est que l'établissement de l'égalité en comparant 50 champs est un processus long et gourmand en ressources. Imaginons maintenant qu'en plus de notre Manclasse, nous ayons une Womanclasse avec exactement les mêmes champs qui existent dans Man. Si un autre programmeur utilise nos classes, il ou elle pourrait facilement écrire un code comme celui-ci :

public static void main(String[] args) {
  
   Man man = new Man(........); // A bunch of parameters in the constructor

   Woman woman = new Woman(.........); // The same bunch of parameters.

   System.out.println(man.equals(woman));
}
Dans ce cas, vérifier les valeurs des champs est inutile : on voit bien qu'on a des objets de deux classes différentes, donc il n'y a pas moyen qu'ils soient égaux ! Cela signifie que nous devons ajouter une vérification à la equals()méthode, en comparant les classes des objets comparés. C'est bien qu'on y ait pensé !

@Override
public boolean equals(Object o) {
   if (getClass() != o.getClass()) return false;
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
Mais peut-être avons-nous oublié autre chose ? Hmm... Au minimum, il faudrait vérifier qu'on ne compare pas un objet avec lui-même ! Si les références A et B pointent vers la même adresse mémoire, alors elles sont le même objet, et nous n'avons pas besoin de perdre du temps et de comparer 50 champs.

@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (getClass() != o.getClass()) return false;
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
Cela ne fait pas de mal non plus d'ajouter une vérification pour null: aucun objet ne peut être égal à null. Ainsi, si le paramètre de méthode est nul, il est inutile de procéder à des vérifications supplémentaires. Avec tout cela à l'esprit, notre equals()méthode pour la Manclasse ressemble à ceci :

@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (o == null || getClass() != o.getClass()) return false;
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
Nous effectuons toutes les vérifications initiales mentionnées ci-dessus. En fin de journée, si :
  • nous comparons deux objets de la même classe
  • et les objets comparés ne sont pas le même objet
  • et l'objet passé n'est pasnull
...puis nous procédons à une comparaison des caractéristiques pertinentes. Pour nous, cela signifie les dnaCodechamps des deux objets. Lorsque vous remplacez la equals()méthode, veillez à respecter ces exigences :
  1. Réflexivité.

    Lorsque la equals()méthode est utilisée pour comparer n'importe quel objet avec lui-même, elle doit renvoyer true.
    Nous nous sommes déjà conformés à cette exigence. Notre méthode comprend :

    
    if (this == o) return true;
    

  2. Symétrie.

    Si a.equals(b) == true, alors b.equals(a)doit revenir true.
    Notre méthode satisfait également à cette exigence.

  3. Transitivité.

    Si deux objets sont égaux à un troisième objet, alors ils doivent être égaux l'un à l'autre.
    Si a.equals(b) == trueet a.equals(c) == true, alors b.equals(c)doit aussi retourner vrai.

  4. Persistance.

    Le résultat de equals()doit changer uniquement lorsque les champs concernés sont modifiés. Si les données des deux objets ne changent pas, le résultat de equals()doit toujours être le même.

  5. Inégalité avec null.

    Pour tout objet, a.equals(null)doit retourner false
    Il ne s'agit pas seulement d'un ensemble de "recommandations utiles", mais plutôt d'un contrat strict , énoncé dans la documentation Oracle

méthode hashCode()

Parlons maintenant de la hashCode()méthode. Pourquoi est-ce nécessaire ? Dans le même but : comparer des objets. Mais nous l'avons déjà equals()! Pourquoi une autre méthode ? La réponse est simple : améliorer les performances. Une fonction de hachage, représentée en Java à l'aide de la hashCode()méthode, renvoie une valeur numérique de longueur fixe pour tout objet. En Java, la hashCode()méthode renvoie un nombre 32 bits ( int) pour tout objet. La comparaison de deux nombres est beaucoup plus rapide que la comparaison de deux objets à l'aide de la equals()méthode, surtout si cette méthode prend en compte de nombreux champs. Si notre programme compare des objets, c'est beaucoup plus simple à faire en utilisant un code de hachage. Ce n'est que si les objets sont égaux en fonction de la hashCode()méthode que la comparaison passe à laequals()méthode. Soit dit en passant, c'est ainsi que fonctionnent les structures de données basées sur le hachage, par exemple, le familier HashMap! La hashCode()méthode, comme la equals()méthode, est remplacée par le développeur. Et tout comme equals(), la hashCode()méthode a des exigences officielles énoncées dans la documentation Oracle :
  1. Si deux objets sont égaux (c'est-à-dire que la equals()méthode renvoie true), alors ils doivent avoir le même code de hachage.

    Sinon, nos méthodes n'auraient aucun sens. Comme nous l'avons mentionné ci-dessus, une hashCode()vérification doit être effectuée en premier pour améliorer les performances. Si les codes de hachage étaient différents, la vérification renverrait faux, même si les objets sont en fait égaux selon la façon dont nous avons défini la equals()méthode.

  2. Si la hashCode()méthode est appelée plusieurs fois sur le même objet, elle doit retourner le même numéro à chaque fois.

  3. La règle 1 ne fonctionne pas dans le sens opposé. Deux objets différents peuvent avoir le même code de hachage.

La troisième règle est un peu déroutante. Comment se peut-il? L'explication est assez simple. La hashCode()méthode retourne un int. An intest un nombre de 32 bits. Il a une plage de valeurs limitée : de -2 147 483 648 à +2 147 483 647. En d'autres termes, il y a un peu plus de 4 milliards de valeurs possibles pour un int. Imaginez maintenant que vous créez un programme pour stocker des données sur toutes les personnes vivant sur Terre. Chaque personne correspondra à son propre Personobjet (semblable à la Manclasse). Il y a environ 7,5 milliards de personnes vivant sur la planète. En d'autres termes, peu importe l'intelligence de l'algorithme que nous écrivons pour convertirPersonobjets à un int, nous n'avons tout simplement pas assez de nombres possibles. Nous n'avons que 4,5 milliards de valeurs int possibles, mais il y a beaucoup plus de personnes que cela. Cela signifie que peu importe nos efforts, certaines personnes différentes auront les mêmes codes de hachage. Lorsque cela se produit (les codes de hachage coïncident pour deux objets différents), nous appelons cela une collision. Lors du remplacement de la hashCode()méthode, l'un des objectifs du programmeur est de minimiser le nombre potentiel de collisions. Compte tenu de toutes ces règles, à quoi hashCode()ressemblera la méthode dans la Personclasse ? Comme ça:

@Override
public int hashCode() {
   return dnaCode;
}
Surpris? :) Si vous regardez les exigences, vous verrez que nous les respectons toutes. Les objets pour lesquels notre equals()méthode renvoie true seront également égaux selon hashCode(). Si nos deux Personobjets sont égaux en equals(c'est-à-dire qu'ils ont le même dnaCode), alors notre méthode renvoie le même nombre. Prenons un exemple plus difficile. Supposons que notre programme sélectionne des voitures de luxe pour les collectionneurs de voitures. Collectionner peut être un passe-temps complexe avec de nombreuses particularités. Une voiture de 1963 peut coûter 100 fois plus qu'une voiture de 1964. Une voiture rouge de 1970 peut coûter 100 fois plus cher qu'une voiture bleue de la même marque de la même année. méthodes equals et hashCode : bonnes pratiques - 4Dans notre exemple précédent, avec la Personclasse, nous avons rejeté la plupart des champs (c'est-à-dire les caractéristiques humaines) comme étant insignifiants et n'avons utilisé que lesdnaCodeterrain dans les comparaisons. Nous travaillons maintenant dans un domaine très idiosyncratique, dans lequel il n'y a pas de détails insignifiants ! Voici notre LuxuryAutoclasse :

public class LuxuryAuto {

   private String model;
   private int manufactureYear;
   private int dollarPrice;

   public LuxuryAuto(String model, int manufactureYear, int dollarPrice) {
       this.model = model;
       this.manufactureYear = manufactureYear;
       this.dollarPrice = dollarPrice;
   }

   // ...getters, setters, etc.
}
Maintenant, nous devons considérer tous les champs dans nos comparaisons. Toute erreur pourrait coûter des centaines de milliers de dollars à un client, il serait donc préférable d'être trop prudent :

@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (o == null || getClass() != o.getClass()) return false;

   LuxuryAuto that = (LuxuryAuto) o;

   if (manufactureYear != that.manufactureYear) return false;
   if (dollarPrice != that.dollarPrice) return false;
   return model.equals(that.model);
}
Dans notre equals()méthode, nous n'avons pas oublié tous les contrôles dont nous avons parlé plus tôt. Mais maintenant nous comparons chacun des trois champs de nos objets. Pour ce programme, nous avons besoin d'une égalité absolue, c'est-à-dire l'égalité de chaque champ. Qu'en est-il hashCode?

@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = result + manufactureYear;
   result = result + dollarPrice;
   return result;
}
Le modelchamp de notre classe est une chaîne. C'est pratique, car la Stringclasse remplace déjà la hashCode()méthode. Nous calculons le modelcode de hachage du champ, puis y ajoutons la somme des deux autres champs numériques. Les développeurs Java ont une astuce simple qu'ils utilisent pour réduire le nombre de collisions : lors du calcul d'un code de hachage, multipliez le résultat intermédiaire par un nombre premier impair. Le nombre le plus couramment utilisé est 29 ou 31. Nous n'approfondirons pas les subtilités mathématiques pour l'instant, mais à l'avenir rappelez-vous que multiplier les résultats intermédiaires par un nombre impair suffisamment grand permet d'"étaler" les résultats de la fonction de hachage et, par conséquent, réduisez le nombre d'objets avec le même code de hachage. Pour notre hashCode()méthode dans LuxuryAuto, cela ressemblerait à ceci :

@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = 31 * result + manufactureYear;
   result = 31 * result + dollarPrice;
   return result;
}
Vous pouvez en savoir plus sur toutes les subtilités de ce mécanisme dans cet article sur StackOverflow , ainsi que dans le livre Effective Java de Joshua Bloch. Enfin, un autre point important qui mérite d'être mentionné. Chaque fois que nous avons remplacé les méthodes equals()et hashCode(), nous avons sélectionné certains champs d'instance qui sont pris en compte dans ces méthodes. Ces méthodes considèrent les mêmes champs. Mais peut-on envisager différents domaines dans equals()et hashCode()? Techniquement, nous pouvons. Mais c'est une mauvaise idée, et voici pourquoi :

@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (o == null || getClass() != o.getClass()) return false;

   LuxuryAuto that = (LuxuryAuto) o;

   if (manufactureYear != that.manufactureYear) return false;
   return dollarPrice == that.dollarPrice;
}

@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = 31 * result + manufactureYear;
   result = 31 * result + dollarPrice;
   return result;
}
Voici nos méthodes equals()et hashCode()pour la LuxuryAutoclasse. La hashCode()méthode est restée inchangée, mais nous avons supprimé le modelchamp de la equals()méthode. Le modèle n'est plus une caractéristique utilisée lorsque la equals()méthode compare deux objets. Mais lors du calcul du code de hachage, ce champ est toujours pris en compte. Qu'obtenons-nous en conséquence ? Créons deux voitures et découvrons-le !

public class Main {

   public static void main(String[] args) {

       LuxuryAuto ferrariGTO = new LuxuryAuto("Ferrari 250 GTO", 1963, 70000000);
       LuxuryAuto ferrariSpider = new LuxuryAuto("Ferrari 335 S Spider Scaglietti", 1963, 70000000);

       System.out.println("Are these two objects equal to each other?");
       System.out.println(ferrariGTO.equals(ferrariSpider));

       System.out.println("What are their hash codes?");
       System.out.println(ferrariGTO.hashCode());
       System.out.println(ferrariSpider.hashCode());
   }
}

Are these two objects equal to each other? 
true 
What are their hash codes? 
-1372326051 
1668702472
Erreur! En utilisant des champs différents pour les méthodes equals()et hashCode(), nous avons violé les contrats qui ont été établis pour eux ! Deux objets égaux selon la equals()méthode doivent avoir le même code de hachage. Nous avons reçu des valeurs différentes pour eux. De telles erreurs peuvent entraîner des conséquences absolument incroyables, en particulier lorsque vous travaillez avec des collections qui utilisent un hachage. Par conséquent, lorsque vous remplacez equals()et hashCode(), vous devez considérer les mêmes champs. Cette leçon a été plutôt longue, mais vous avez beaucoup appris aujourd'hui ! :) Il est maintenant temps de revenir à la résolution de tâches !
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION