JavaRush /Blog Java /Random-FR /Contrats égaux et hashCode ou quoi que ce soit
Aleksandr Zimin
Niveau 1
Санкт-Петербург

Contrats égaux et hashCode ou quoi que ce soit

Publié dans le groupe Random-FR
Bien entendu, la grande majorité des programmeurs Java savent que les méthodes equalssont hashCodeétroitement liées les unes aux autres et qu'il est conseillé de remplacer systématiquement ces deux méthodes dans leurs classes. Un nombre légèrement inférieur de personnes savent pourquoi il en est ainsi et quelles tristes conséquences peuvent survenir si cette règle n'est pas respectée. Je propose de réfléchir au concept de ces méthodes, de répéter leur objectif et de comprendre pourquoi elles sont si liées. J'ai écrit cet article, comme le précédent sur le chargement des classes, pour moi-même afin de révéler enfin tous les détails du problème et de ne plus revenir à des sources tierces. Par conséquent, je serai heureux de recevoir des critiques constructives, car s’il y a des lacunes quelque part, elles doivent être éliminées. L'article, hélas, s'est avéré assez long.

est égal aux règles de remplacement

Une méthode equals()est nécessaire en Java pour confirmer ou infirmer le fait que deux objets de même origine sont logiquement égaux . Autrement dit, lorsqu'il compare deux objets, le programmeur doit comprendre si leurs champs significatifs sont équivalents . Il n'est pas nécessaire que tous les champs soient identiques, puisque la méthode equals()implique une égalité logique . Mais parfois, il n’est pas particulièrement nécessaire d’utiliser cette méthode. Comme on dit, le moyen le plus simple d'éviter les problèmes en utilisant un mécanisme particulier est de ne pas l'utiliser. Il convient également de noter qu’une fois que vous rompez un contrat, equalsvous perdez le contrôle de la compréhension de la manière dont les autres objets et structures interagiront avec votre objet. Et par la suite, trouver la cause de l'erreur sera très difficile.

Quand ne pas remplacer cette méthode

  • Lorsque chaque instance d’une classe est unique.
  • Dans une plus large mesure, cela s'applique aux classes qui fournissent un comportement spécifique plutôt que d'être conçues pour fonctionner avec des données. Comme par exemple la classe Thread. Pour eux equals, l’implémentation de la méthode fournie par la classe Objectest largement suffisante. Un autre exemple est celui des classes d'énumération ( Enum).
  • Alors qu’en fait la classe n’est pas tenue de déterminer l’équivalence de ses instances.
  • Par exemple, pour une classe, java.util.Randomil n'est pas du tout nécessaire de comparer les instances de la classe entre elles, pour déterminer si elles peuvent renvoyer la même séquence de nombres aléatoires. Tout simplement parce que la nature de cette classe n’implique même pas un tel comportement.
  • Lorsque la classe que vous étendez a déjà sa propre implémentation de la méthode equalset que le comportement de cette implémentation vous convient.
  • Par exemple, pour les classes Set, List, Mapl'implémentation equalsest respectivement dans AbstractSet, AbstractListet AbstractMap.
  • Et enfin, il n'est pas nécessaire de surcharger equalslorsque la portée de votre classe est privateou package-privateet vous êtes sûr que cette méthode ne sera jamais appelée.

est égal à un contrat

Lors du remplacement d'une méthode, equalsle développeur doit respecter les règles de base définies dans la spécification du langage Java.
  • Réflexivité
  • pour toute valeur donnée x, l'expression x.equals(x)doit renvoyer true.
    Étant donné - c'est-à-dire tel quex != null
  • Symétrie
  • pour toute valeur donnée xet y, x.equals(y)ne devrait revenir trueque s'il y.equals(x)renvoie true.
  • Transitivité
  • pour toute valeur donnée x, yet z, si x.equals(y)renvoie trueet y.equals(z)renvoie true, x.equals(z)doit renvoyer la valeur true.
  • Cohérence
  • pour toute valeur donnée, xet yl'appel répété x.equals(y)renverra la valeur de l'appel précédent à cette méthode, à condition que les champs utilisés pour comparer les deux objets n'aient pas changé entre les appels.
  • Comparaison nulle
  • pour toute valeur donnée, xl'appel x.equals(null)doit renvoyer false.

équivaut à une violation du contrat

De nombreuses classes, comme celles du Java Collections Framework, dépendent de l'implémentation de la méthode equals(), il ne faut donc pas la négliger, car La violation du contrat de cette méthode peut conduire à un fonctionnement irrationnel de l'application, et dans ce cas, il sera assez difficile d'en trouver la raison. Selon le principe de réflexivité , tout objet doit être équivalent à lui-même. Si ce principe n'est pas respecté, lorsque nous ajoutons un objet à la collection puis le recherchons à l'aide de la méthode, contains()nous ne pourrons pas trouver l'objet que nous venons d'ajouter à la collection. La condition de symétrie stipule que deux objets doivent être égaux quel que soit l’ordre dans lequel ils sont comparés. Par exemple, si vous avez une classe contenant un seul champ de type chaîne, il sera incorrect de comparer equalsce champ avec une chaîne dans une méthode. Parce que dans le cas d'une comparaison inverse, la méthode renverra toujours la valeur false.
// Нарушение симметричности
public class SomeStringify {
    private String s;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o instanceof SomeStringify) {
            return s.equals(((SomeStringify) o).s);
        }
        // нарушение симметричности, классы разного происхождения
        if (o instanceof String) {
            return s.equals(o);
        }
        return false;
    }
}
//Правильное определение метода equals
@Override
public boolean equals(Object o) {
    if (this == o) return true;
    return o instanceof SomeStringify &&
            ((SomeStringify) o).s.equals(s);
}
De la condition de transitivité, il s'ensuit que si deux objets sur trois sont égaux, alors dans ce cas, tous les trois doivent être égaux. Ce principe peut facilement être violé lorsqu'il est nécessaire d'étendre une certaine classe de base en y ajoutant un composant significatif . Par exemple, à une classe Pointavec des coordonnées xet yvous devez ajouter la couleur du point en le développant. Pour ce faire, vous devrez déclarer une classe ColorPointavec le champ approprié color. Ainsi, si dans la classe étendue nous appelons la equalsméthode parent, et dans le parent nous supposons que seules les coordonnées xet sont comparées y, alors deux points de couleurs différentes mais avec les mêmes coordonnées seront considérés comme égaux, ce qui est incorrect. Dans ce cas, il faut apprendre à la classe dérivée à distinguer les couleurs. Pour ce faire, vous pouvez utiliser deux méthodes. Mais l'un violera la règle de symétrie , et l'autre la transitivité .
// Первый способ, нарушая симметричность
// Метод переопределен в классе ColorPoint
@Override
public boolean equals(Object o) {
    if (!(o instanceof ColorPoint)) return false;
    return super.equals(o) && ((ColorPoint) o).color == color;
}
Dans ce cas, l'appel point.equals(colorPoint)renverra la valeur true, et la comparaison colorPoint.equals(point)renverra false, car attend un objet de « sa » classe. La règle de symétrie est donc violée. La deuxième méthode consiste à effectuer une vérification « aveugle » dans le cas où il n'y a pas de données sur la couleur du point, c'est-à-dire que nous avons la classe Point. Ou vérifiez la couleur si des informations la concernant sont disponibles, c'est-à-dire comparez un objet de la classe ColorPoint.
// Метод переопределен в классе ColorPoint
@Override
public boolean equals(Object o) {
    if (!(o instanceof Point)) return false;

    // Слепая проверка
    if (!(o instanceof ColorPoint))
        return super.equals(o);

    // Полная проверка, включая цвет точки
    return super.equals(o) && ((ColorPoint) o).color == color;
}
Le principe de transitivité est ici violé de la manière suivante. Disons qu'il existe une définition des objets suivants :
ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
Point p2 = new Point(1, 2);
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);
Ainsi, même si l'égalité p1.equals(p2)et est vraie p2.equals(p3), p1.equals(p3)elle renverra la valeur false. En même temps, la deuxième méthode, à mon avis, semble moins attrayante, car Dans certains cas, l'algorithme peut être aveugle et ne pas effectuer la comparaison entièrement, et vous ne le savez peut-être pas. Un peu de poésie D'une manière générale, d'après ce que je comprends, il n'existe pas de solution concrète à ce problème. Il existe une opinion d'un auteur faisant autorité nommé Kay Horstmann selon laquelle vous pouvez remplacer l'utilisation de l'opérateur instanceofpar un appel de méthode getClass()qui renvoie la classe de l'objet et, avant de commencer à comparer les objets eux-mêmes, assurez-vous qu'ils sont du même type. , et ne prêtent pas attention au fait de leur origine commune. Ainsi, les règles de symétrie et de transitivité seront satisfaites. Mais en même temps, de l'autre côté de la barricade se trouve un autre auteur, non moins respecté dans de larges cercles, Joshua Bloch, qui estime que cette approche viole le principe de substitution de Barbara Liskov. Ce principe stipule que « le code appelant doit traiter une classe de base de la même manière que ses sous-classes sans la connaître » . Et dans la solution proposée par Horstmann, ce principe est clairement violé, puisqu'il dépend de la mise en œuvre. Bref, force est de constater que l’affaire est sombre. Il convient également de noter que Horstmann clarifie la règle d'application de son approche et écrit en anglais simple que vous devez décider d'une stratégie lors de la conception des classes, et si les tests d'égalité seront effectués uniquement par la superclasse, vous pouvez le faire en effectuant l'opération instanceof. Sinon, lorsque la sémantique de la vérification change en fonction de la classe dérivée et que l'implémentation de la méthode doit être déplacée vers le bas de la hiérarchie, vous devez utiliser la méthode getClass(). Joshua Bloch, à son tour, propose d'abandonner l'héritage et d'utiliser la composition d'objets en incluant une ColorPointclasse dans la classe Pointet en fournissant une méthode d'accès asPoint()pour obtenir des informations spécifiquement sur le point. Cela évitera d’enfreindre toutes les règles, mais, à mon avis, cela rendra le code plus difficile à comprendre. La troisième option consiste à utiliser la génération automatique de la méthode égale à l'aide de l'EDI. Idea, d'ailleurs, reproduit la génération Horstmann, vous permettant de choisir une stratégie d'implémentation d'une méthode dans une superclasse ou dans ses descendants. Enfin, la règle de cohérence suivante stipule que même si les objets ne changent xpas y, les appeler à nouveau x.equals(y)doit renvoyer la même valeur qu'avant. La règle finale est qu’aucun objet ne doit être égal à null. Tout est clair ici null: c'est l'incertitude, l'objet est-il égal à l'incertitude ? Ce n'est pas clair, c'est-à-dire false.

Algorithme général pour déterminer les égaux

  1. Vérifiez l'égalité des références d'objet thiset des paramètres de méthode o.
    if (this == o) return true;
  2. Vérifiez si le lien est défini o, c'est-à-dire s'il l'est null.
    Si à l'avenir, lors de la comparaison des types d'objets, l'opérateur sera utilisé instanceof, cet élément peut être ignoré, car ce paramètre est renvoyé falsedans ce cas null instanceof Object.
  3. Comparez les types d'objets thisà l'aide od'un opérateur instanceofou d'une méthode getClass(), guidé par la description ci-dessus et votre propre intuition.
  4. Si une méthode equalsest remplacée dans une sous-classe, assurez-vous de passer un appelsuper.equals(o)
  5. Convertissez le type de paramètre oen classe requise.
  6. Effectuez une comparaison de tous les champs d'objet significatifs :
    • pour les types primitifs (sauf floatet double), en utilisant l'opérateur==
    • pour les champs de référence, vous devez appeler leur méthodeequals
    • pour les tableaux, vous pouvez utiliser l'itération cyclique ou la méthodeArrays.equals()
    • pour les types floatet doubleil est nécessaire d'utiliser des méthodes de comparaison des classes wrapper correspondantes Float.compare()etDouble.compare()
  7. Et enfin, répondez à trois questions : la méthode mise en œuvre est-elle symétrique ? Transitif ? Convenu ? Les deux autres principes ( réflexivité et certitude ) s'exécutent généralement automatiquement.

Règles de remplacement du HashCode

Un hachage est un nombre généré à partir d'un objet qui décrit son état à un moment donné. Ce numéro est utilisé en Java principalement dans les tables de hachage telles que HashMap. Dans ce cas, la fonction de hachage consistant à obtenir un nombre basé sur un objet doit être implémentée de manière à assurer une répartition relativement uniforme des éléments dans la table de hachage. Et aussi pour minimiser le risque de collision lorsque la fonction renvoie la même valeur pour différentes clés.

Code de hachage du contrat

Pour implémenter une fonction de hachage, la spécification du langage définit les règles suivantes :
  • appeler une méthode hashCodeune ou plusieurs fois sur le même objet doit renvoyer la même valeur de hachage, à condition que les champs de l'objet impliqués dans le calcul de la valeur n'aient pas changé.
  • l'appel d'une méthode hashCodesur deux objets doit toujours renvoyer le même nombre si les objets sont égaux (l'appel d'une méthode equalssur ces objets renvoie true).
  • l'appel d'une méthode hashCodesur deux objets inégaux doit renvoyer des valeurs de hachage différentes. Bien que cette exigence ne soit pas obligatoire, il convient de considérer que sa mise en œuvre aura un effet positif sur les performances des tables de hachage.

Les méthodes equals et hashCode doivent être remplacées ensemble

Sur la base des contrats décrits ci-dessus, il s'ensuit que lorsque vous remplacez la méthode dans votre code equals, vous devez toujours remplacer la méthode hashCode. Puisqu'en fait deux instances d'une classe sont différentes parce qu'elles se trouvent dans des zones mémoire différentes, elles doivent être comparées selon certains critères logiques. Par conséquent, deux objets logiquement équivalents doivent renvoyer la même valeur de hachage. Que se passe-t-il si une seule de ces méthodes est remplacée ?
  1. equalsOui hashCodeNon

    Disons que nous avons correctement défini une méthode equalsdans notre classe et hashCodeavons décidé de la laisser telle quelle dans la classe Object. Alors du point de vue de la méthode equalsles deux objets seront logiquement égaux, alors que du point de vue de la méthode hashCodeils n'auront rien en commun. Et ainsi, en plaçant un objet dans une table de hachage, on court le risque de ne pas le récupérer par clé.
    Par exemple, comme ceci :

    Map<Point, String> m = new HashMap<>();
    m.put(new Point(1, 1),Point A);
    // pointName == null
    String pointName = m.get(new Point(1, 1));

    Évidemment, l’objet placé et l’objet recherché sont deux objets différents, bien qu’ils soient logiquement égaux. Mais parce que ils ont des valeurs de hachage différentes parce que nous avons violé le contrat, on peut dire que nous avons perdu notre objet quelque part dans les entrailles de la table de hachage.

  2. hashCodeOui equalsNon.

    Que se passe-t-il si nous remplaçons la méthode hashCodeet equalshéritons de l'implémentation de la méthode de la classe Object. Comme vous le savez, la equalsméthode par défaut compare simplement les pointeurs vers des objets, déterminant s'ils font référence au même objet. Supposons que hashCodenous ayons écrit la méthode selon tous les canons, à savoir que nous l'ayons générée à l'aide de l'EDI, et qu'elle renverra les mêmes valeurs de hachage pour des objets logiquement identiques. Évidemment, ce faisant, nous avons déjà défini un mécanisme permettant de comparer deux objets.

    C’est pourquoi l’exemple du paragraphe précédent devrait en théorie être appliqué. Mais nous ne pourrons toujours pas retrouver notre objet dans la table de hachage. Bien que nous en soyons proches, car au minimum nous trouverons le panier de la table de hachage dans lequel se trouvera l'objet.

    Pour rechercher avec succès un objet dans une table de hachage, en plus de comparer les valeurs de hachage de la clé, la détermination de l'égalité logique de la clé avec l'objet recherché est également utilisée. Autrement dit, equalsil n’y a aucun moyen de se passer de la substitution de la méthode.

Algorithme général pour déterminer hashCode

Ici, il me semble qu’il ne faut pas trop s’inquiéter et générer la méthode dans votre IDE préféré. Parce que tous ces déplacements de bits vers la droite et la gauche à la recherche du nombre d'or, c'est-à-dire une distribution normale, c'est pour les mecs complètement têtus. Personnellement, je doute de pouvoir faire mieux et plus vite que la même idée.

Au lieu d'une conclusion

Ainsi, nous voyons que les méthodes equalsjouent hashCodeun rôle bien défini dans le langage Java et sont conçues pour obtenir la caractéristique d'égalité logique de deux objets. Dans le cas de la méthode, equalscela a un rapport direct avec la comparaison d'objets, dans le cas d' hashCodeune comparaison indirecte, lorsqu'il est nécessaire, par exemple, de déterminer l'emplacement approximatif d'un objet dans des tables de hachage ou des structures de données similaires afin de augmenter la vitesse de recherche d'un objet. En plus des contrats , equalsil hashCodeexiste une autre exigence liée à la comparaison d'objets. C'est la cohérence d'une méthode compareTod'interface Comparableavec un equals. Cette exigence oblige le développeur à toujours revenir x.equals(y) == truequand x.compareTo(y) == 0. Autrement dit, nous voyons que la comparaison logique de deux objets ne doit se contredire nulle part dans l’application et doit toujours être cohérente.

Sources

Java efficace, deuxième édition. Josué Bloch. Traduction gratuite d'un très bon livre. Java, une bibliothèque professionnelle. Tome 1. Les bases. Kay Horstmann. Un peu moins de théorie et plus de pratique. Mais tout n’est pas analysé avec autant de détails que celui de Bloch. Bien qu'il existe une vue sur le même égal(). Structures de données en images. HashMap Un article extrêmement utile sur le périphérique HashMap en Java. Au lieu de regarder les sources.
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION