JavaRush /Blog Java /Random-FR /Méthodes equals & hashCode : pratique d'utilisation

Méthodes equals & hashCode : pratique d'utilisation

Publié dans le groupe Random-FR
Bonjour! 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 : au début du cours JavaRush, il y avait une courte conférence sur equals()- lisez-la si vous l'avez oublié ou si vous ne l'avez pas vue auparavant. Méthodes égales et amp;  hashCode : pratique d'utilisation - 1Dans la leçon d'aujourd'hui, nous parlerons de ces concepts en détail - croyez-moi, il y a beaucoup de choses à dire ! Et avant de passer à quelque chose de nouveau, rafraîchissons notre mémoire sur ce que nous avons déjà abordé :) Comme vous vous en souvenez, la comparaison habituelle de deux objets à l'aide de l' ==opérateur « » est une mauvaise idée, car «== » compare des références. Voici notre exemple avec des voitures d'une conférence 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 de la console :

false
Il semblerait que nous ayons créé deux objets identiques de la classe Car: tous les champs sur les deux machines sont identiques, mais le résultat de la comparaison est toujours faux. Nous connaissons déjà la raison : les liens car1pointent car2vers des adresses différentes en mémoire, ils ne sont donc pas égaux. Nous voulons toujours comparer deux objets, pas deux références. La meilleure solution pour comparer des objets est le equals().

méthode égale()

Vous vous souvenez peut-être que nous ne créons pas cette méthode à partir de zéro, mais que nous la remplaçons - après tout, la méthode equals()est définie dans la classe Object. Cependant, sous sa forme habituelle, il est de peu d'utilité :
public boolean equals(Object obj) {
   return (this == obj);
}
C'est ainsi que la méthode equals()est définie dans la classe Object. La même comparaison de liens. Pourquoi a-t-il été fait ainsi ? 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 l'idée principale de la méthode equals()- le créateur de la classe détermine lui-même les caractéristiques par lesquelles l'égalité des objets de cette classe est vérifiée. En faisant cela, vous remplacez la méthode equals()dans votre classe. Si vous ne comprenez pas bien le sens de « vous définissez vous-même les caractéristiques », regardons un exemple. Voici une classe simple de personnes - 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.
}
Disons que nous écrivons un programme qui doit déterminer si deux personnes sont liées par des jumeaux ou simplement par 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 d'un test biologique ADN (pour plus de simplicité - sous la forme d'un numéro de code). Selon vous, laquelle de ces caractéristiques permettra à notre programme d'identifier des parents jumeaux ? Méthodes égales et amp;  hashCode : pratique d'utilisation - 2Bien entendu, seul un test biologique peut apporter une garantie. Deux personnes peuvent avoir la même couleur des yeux, la même coiffure, le même nez et même les mêmes cicatrices - il existe de nombreuses personnes dans le monde et il est impossible d'éviter les coïncidences. Il nous faut un mécanisme fiable : seul le résultat d’un test ADN permet de tirer une conclusion précise. Qu'est-ce que cela signifie pour notre méthode equals()? Nous devons le redéfinir dans une classe Manen tenant compte des exigences de notre programme. La méthode doit comparer le champ de int dnaCodedeux objets, et 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 aussi simple ? Pas vraiment. Nous avons raté quelque chose. Dans ce cas, pour nos objets nous n'avons défini qu'un seul champ « significatif » par lequel leur égalité est établie - dnaCode. Imaginez maintenant que nous aurions non pas 1, mais 50 champs « significatifs ». Et si les 50 champs de deux objets sont égaux, alors les objets sont égaux. Cela pourrait également arriver. Le principal problème est que le calcul de l’égalité de 50 champs est un processus long et gourmand en ressources. Imaginez maintenant qu'en plus de la classe, Mannous ayons une classe Womanavec exactement les mêmes champs que dans Man. Et si un autre programmeur utilise vos classes, il peut facilement écrire dans son programme quelque chose comme :
public static void main(String[] args) {

   Man man = new Man(........); //a bunch of parameters in the constructor

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

   System.out.println(man.equals(woman));
}
Dans ce cas, cela ne sert à rien de vérifier les valeurs des champs : on voit que l'on regarde des objets de deux classes différentes, et ils ne peuvent pas être égaux en principe ! Cela signifie que nous devons effectuer une vérification dans la méthode equals(): une comparaison d'objets de deux classes identiques. C'est bien qu'on ait pensé à ça !
@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... Il faudrait au minimum vérifier qu'on ne compare pas l'objet avec lui-même ! Si les références A et B pointent vers la même adresse en mémoire, alors il s’agit du même objet et nous n’avons pas non plus besoin de perdre du temps à 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;
}
De plus, cela ne ferait pas de mal d'ajouter une vérification pour null: aucun objet ne peut être égal à null, auquel cas des vérifications supplémentaires ne servent à rien. En tenant compte de tout cela, notre méthode equals()de classe Manressemblera à 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. S'il s'avère que :
  • on compare deux objets de la même classe
  • ce n'est pas le même objet
  • nous ne comparons pas notre objet avecnull
...puis nous passons à la comparaison des caractéristiques significatives. Dans notre cas, les champs dnaCodede deux objets. Lorsque vous remplacez une méthode equals(), assurez-vous de respecter ces exigences :
  1. Réflexivité.

    Tout objet doit être equals()à lui-même.
    Nous avons déjà pris en compte cette exigence. Notre méthode indique :

    if (this == o) return true;

  2. Symétrie.

    Si a.equals(b) == true, alors b.equals(a)il devrait revenir true.
    Notre méthode répond é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 la vérification b.equals(c)doit également renvoyer vrai.

  4. La permanence.

    Les résultats du travail equals()ne devraient changer que lorsque les champs qui y sont inclus changent. Si les données de deux objets n'ont pas changé, les résultats du contrôle equals()doivent toujours être les mêmes.

  5. Inégalité avec null.

    Pour tout objet, la vérification a.equals(null)doit renvoyer false.
    Il ne s'agit pas simplement d'un ensemble de « recommandations utiles », mais d'un contrat strict de méthodes , prescrit dans la documentation Oracle.

Méthode hashCode()

Parlons maintenant de la méthode hashCode(). Pourquoi est-ce nécessaire ? Exactement 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 la productivité. Une fonction de hachage, représentée par la méthode , en Java hashCode(), renvoie une valeur numérique de longueur fixe pour tout objet. Dans le cas de Java, la méthode hashCode()renvoie un nombre de 32 bits de type int. Comparer deux nombres entre eux est beaucoup plus rapide que comparer deux objets à l'aide de la méthode equals(), surtout si elle utilise de nombreux champs. Si notre programme compare des objets, il est beaucoup plus facile de le faire par code de hachage, et seulement s'ils sont égaux hashCode()- procédez à la comparaison par equals(). C’est d’ailleurs ainsi que fonctionnent les structures de données basées sur le hachage, par exemple celle que vous connaissez HashMap! La méthode hashCode(), tout comme equals(), est remplacée par le développeur lui-même. Et tout comme pour equals(), la méthode hashCode()a des exigences officielles spécifiées dans la documentation Oracle :
  1. Si deux objets sont égaux (c’est-à-dire que la méthode equals()renvoie vrai), ils doivent avoir le même code de hachage.

    Autrement, nos méthodes n’auront aucun sens. La vérification par hashCode(), comme nous l'avons dit, devrait venir en premier pour améliorer les performances. Si les codes de hachage sont différents, la vérification retournera faux, même si les objets sont en réalité égaux (comme nous l'avons défini dans la méthode equals()).

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

  3. La règle 1 ne fonctionne pas à l’envers. 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 méthode hashCode()renvoie int. intest un nombre de 32 bits. Il a un nombre limité de valeurs - de -2 147 483 648 à +2 147 483 647. En d’autres termes, il existe un peu plus de 4 milliards de variations du nombre int. Imaginez maintenant que vous créez un programme pour stocker des données sur toutes les personnes vivantes sur Terre. Chaque personne aura son propre objet de classe Man. Environ 7,5 milliards de personnes vivent sur Terre. En d’autres termes, quelle que soit la qualité de l’algorithme Manque nous écrivons pour convertir des objets en nombres, nous n’aurons tout simplement pas assez de nombres. Nous n’avons que 4,5 milliards d’options, et bien plus de personnes. Cela signifie que peu importe nos efforts, les codes de hachage seront les mêmes pour différentes personnes. Cette situation (les codes de hachage de deux objets différents correspondant) est appelée une collision. L'un des objectifs du programmeur lors de la substitution d'une méthode hashCode()est de réduire autant que possible le nombre potentiel de collisions. À quoi ressemblera notre méthode hashCode()pour la classe Man, en tenant compte de toutes ces règles ? Comme ça:
@Override
public int hashCode() {
   return dnaCode;
}
Surpris? :) De façon inattendue, mais si vous regardez les exigences, vous verrez que nous respectons tout. Les objets pour lesquels le nôtre equals()renvoie true seront égaux dans hashCode(). Si nos deux objets Manont une valeur égale equals(c'est-à-dire qu'ils ont la même valeur dnaCode), notre méthode renverra le même nombre. Regardons un exemple plus compliqué. Disons que notre programme devrait sélectionner des voitures de luxe pour les clients collectionneurs. La collection est une chose complexe et comporte de nombreuses fonctionnalités. Une voiture de 1963 peut coûter 100 fois plus cher que la même 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 égales et amp;  hashCode : pratique d'utilisation - 4Dans le premier cas, avec la classe Man, nous avons écarté la plupart des champs (c'est-à-dire les caractéristiques de la personne) comme étant insignifiants et utilisé uniquement le champ à des fins de comparaison dnaCode. Ici, nous travaillons avec un territoire tout à fait unique, et il ne peut y avoir de petits détails ! Voici notre classe LuxuryAuto:
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.
}
Ici, lors de la comparaison, il faut prendre en compte tous les domaines. Toute erreur peut coûter des centaines de milliers de dollars au client, il vaut donc mieux être 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 méthode, equals()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. Dans ce programme, l'égalité doit être absolue, dans tous les domaines. Qu'en est-il de hashCode?
@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = result + manufactureYear;
   result = result + dollarPrice;
   return result;
}
Le champ modelde notre classe est une chaîne. C'est pratique : Stringla méthode hashCode()est déjà redéfinie dans la classe. Nous calculons le code de hachage du champ modelet nous y ajoutons la somme des deux autres champs numériques. Il existe une petite astuce en Java qui permet de réduire le nombre de collisions : lors du calcul du 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'entrerons pas dans les détails du calcul pour le moment, mais pour référence future, rappelez-vous que multiplier les résultats intermédiaires par un nombre impair suffisamment grand permet de « répartir » les résultats du hachage. fonctionner et se retrouver avec moins d’objets avec le même hashcode. Pour notre méthode hashCode()dans LuxuryAuto, cela ressemblera à 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 de Joshua Bloch « Effective Java ». Enfin, il y a un autre point important qui mérite d’être mentionné. A chaque fois lors du remplacement de equals(), hashCode()nous avons sélectionné certains champs de l'objet, qui ont été pris en compte dans ces méthodes. Mais peut-on prendre en compte 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()pour hashCode()la classe LuxuryAuto. La méthode hashCode()est restée inchangée et equals()nous avons supprimé le champ de la méthode model. Or, le modèle n'est pas une caractéristique permettant de comparer deux objets par equals(). Mais il est toujours pris en compte lors du calcul du hash code. Qu’obtiendrons-nous en conséquence ? Créons deux voitures et vérifions-les !
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());
   }
}

Эти два an object равны друг другу?
true
Какие у них хэш-codeы?
-1372326051
1668702472
Erreur! En utilisant différents champs pour nous equals(), hashCode()nous avons violé le contrat établi pour eux ! Deux objets égaux equals()doivent avoir le même code de hachage. Nous leur donnons des significations différentes. De telles erreurs peuvent entraîner les conséquences les plus incroyables, en particulier lorsque vous travaillez avec des collections utilisant des hachages. Par conséquent, lors de la redéfinition equals(), hashCode()il sera correct d'utiliser les mêmes champs. La conférence s'est avérée assez longue, mais aujourd'hui vous avez appris beaucoup de nouvelles choses ! :) Il est temps de se remettre à résoudre les problèmes !
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION