Une instance pourrait-elle être égale à une autre instance d'un type plus spécifique?


25

J'ai lu cet article: Comment écrire une méthode d'égalité en Java .

Fondamentalement, il fournit une solution pour une méthode equals () qui prend en charge l'héritage:

Point2D twoD   = new Point2D(10, 20);
Point3D threeD = new Point3D(10, 20, 50);
twoD.equals(threeD); // true
threeD.equals(twoD); // true

Mais est-ce une bonne idée? ces deux instances semblent être égales mais peuvent avoir deux codes de hachage différents. N'est-ce pas un peu faux?

Je crois que cela serait mieux réalisé en moulant les opérandes à la place.


1
L'exemple avec des points colorés comme indiqué dans le lien a plus de sens pour moi. Je considérerais qu'un point 2D (x, y) peut être vu comme un point 3D avec une composante zéro Z (x, y, 0), et je voudrais que l'égalité retourne faux dans votre cas. En fait, dans l'article, un ColoredPoint est dit explicitement différent d'un Point et renvoie toujours false.
coredump

10
Rien de pire que des tutoriels qui brisent les conventions courantes ... Il faut des années pour briser ce genre d'habitudes des programmeurs.
corsiKa

3
@coredump Le traitement d'un point 2D comme ayant une zcoordonnée zéro peut être une convention utile pour certaines applications (les premiers systèmes de CAO gérant les données héritées viennent à l'esprit). Mais c'est une convention arbitraire. Les plans dans des espaces de 3 dimensions ou plus peuvent avoir des orientations arbitraires ... c'est ce qui rend les problèmes intéressants intéressants.
ben rudgers

Réponses:


71

Cela ne devrait pas être l'égalité car elle rompt la transitivité . Considérez ces deux expressions:

new Point3D(10, 20, 50).equals(new Point2D(10, 20)) // true
new Point2D(10, 20).equals(new Point3D(10, 20, 60)) // true

L'égalité étant transitive, cela devrait signifier que l'expression suivante est également vraie:

new Point3D(10, 20, 50).equals(new Point3D(10, 20, 60))

Mais bien sûr - ce n'est pas le cas.

Donc, votre idée de coulée est correcte - attendez-vous à ce que, en Java, la coulée signifie simplement couler le type de la référence. Ce que vous voulez vraiment ici, c'est une méthode de conversion qui créera un nouvel Point2Dobjet à partir d'un Point3Dobjet. Cela rendrait également l'expression plus significative:

twoD.equals(threeD.projectXY())

1
L'article décrit des implémentations qui rompent la transitivité et propose une gamme de solutions de contournement. Dans un domaine où nous autorisons les points 2D, nous avons déjà décidé que la troisième dimension n'avait pas d'importance. et donc (10, 20, 50)égal (10, 20, 60)est très bien. Nous nous soucions seulement de 10et 20.
ben rudgers

1
Faut Point2D- il avoir une projectXYZ()méthode pour se Point3Dreprésenter soi-même? En d'autres termes, les implémentations doivent-elles se connaître?
hjk

4
@hjk Se débarrasser Point2Dsemble plus simple car la projection de points 2D nécessite de définir d'abord leur plan dans l'espace 3D. Si le point 2D sait qu'il est plan, c'est déjà un point 3D. Sinon, il ne peut pas projeter. Je me souviens du Flatland d'Abbott .
ben rudgers

@benrudgers Vous pouvez, cependant, définir un Plane3Dobjet, qui définira un plan dans l'espace 3D, ce plan peut avoir une liftméthode (2D-> 3D se soulève, ne se projette pas) qui acceptera un Point2Det un nombre pour le "troisième axe" "- distance de l'avion le long de l'avion normal. Pour faciliter l'utilisation, vous pouvez définir les plans communs comme des constantes statiques, de sorte que vous pouvez faire des choses commePlane3D.XY.lift(new Point2D(10, 20), 50).equals(new Point3D(10, 20, 50))
Idan Arye

@IdanArye Je commentais la suggestion selon laquelle les points 2D devraient avoir une méthode de projection. En ce qui concerne les avions avec des méthodes de portance, je pense qu'il faudrait deux arguments pour avoir du sens: un point 2D et le plan sur lequel il est supposé être, c'est-à-dire qu'il doit vraiment être une projection s'il ne possède pas le point ... et s'il est propriétaire du point, pourquoi ne pas simplement posséder un point 3D et supprimer un type de données problématique et l'odeur d'une méthode maladroite? YMMV.
ben rudgers

10

Je m'éloigne de la lecture de l'article en pensant à la sagesse d'Alan J. Perlis:

Epigramme 9. Il vaut mieux que 100 fonctions fonctionnent sur une seule structure de données que 10 fonctions sur 10 structures de données.

Le fait que le droit à «l'égalité» soit le genre de problème qui empêche l' inventeur de Martin Scala de Scala de dormir la nuit devrait faire réfléchir sur la question de savoir si le remplacement equalsdans un arbre d'héritage est une bonne idée.

Ce qui se passe lorsque nous n'avons pas de chance d'obtenir un, ColoredPointc'est que notre géométrie échoue parce que nous avons utilisé l'héritage pour proliférer les types de données plutôt que d'en créer un bon. Ceci malgré le fait de devoir revenir en arrière et de modifier le nœud racine de l'arbre d'héritage pour que cela equalsfonctionne. Pourquoi ne pas simplement ajouter un zet un colorà Point?

La raison serait que Pointet ColoredPointfonctionner dans différents domaines ... au moins si ces domaines ne se mêlent. Pourtant, si c'est le cas, nous n'avons pas besoin de passer outre equals. Comparer ColoredPointet Pointpour l'égalité n'a de sens que dans un troisième domaine où ils sont autorisés à se mêler. Et dans ce cas, il est probablement préférable d'avoir «l'égalité» adaptée à ce troisième domaine plutôt que d'essayer d'appliquer la sémantique d'égalité de l'un ou l'autre ou des deux domaines non mélangés. En d'autres termes, «l'égalité» devrait être définie localement à l'endroit où nous avons de la boue qui coule des deux côtés, car nous ne voulons peut-être pas ColoredPoint.equals(pt)échouer contre des cas de Pointmême si l'auteur de la ColoredPointpensée était une bonne idée il y a six mois à 2 heures du matin. .


6

Lorsque les anciens dieux de la programmation inventaient la programmation orientée objet avec les classes, ils décidaient, en matière de composition et d'héritage, d'avoir deux relations pour un objet: "est un" et "a un".
Cela a partiellement résolu le problème des sous-classes différentes des classes parentes mais les a rendues utilisables sans casser le code. Parce qu'une instance de sous-classe "est un" objet de super-classe et peut y être substitué directement, même si la sous-classe a plus de fonctions membres ou de membres de données, le "a a" garantit qu'il exécutera toutes les fonctions du parent et aura toutes ses fonctions. membres. On pourrait donc dire qu'un Point3D "est un" Point, et un Point2D "est un" Point s'ils héritent tous les deux de Point. De plus, un Point3D pourrait être une sous-classe de Point2D.

Cependant, l'égalité entre les classes est spécifique au domaine du problème et l'exemple ci-dessus est ambigu quant à ce dont le programmeur a besoin pour que le programme fonctionne correctement. En règle générale, les règles du domaine mathématique sont suivies et les valeurs des données génèrent une égalité si vous limitez la portée de la comparaison à deux dimensions dans ce cas, mais pas si vous comparez tous les membres des données.

Vous obtenez donc un tableau de réduction des égalités:

Both objects have same values, limited to subset of shared members

Child classes can be equal to parent classes if parent and childs
data members are the same.

Both objects entire data members are the same.

Objects must have all same values and be similar classes. 

Objects must have all same values and be the same class type. 

Equality is determined by specific logical conditions in the domain.

Only Objects that both point to same instance are equal. 

Vous choisissez généralement les règles les plus strictes que vous pouvez encore exécuter toutes les fonctions nécessaires dans votre domaine problématique. Les tests d'égalité intégrés pour les nombres sont conçus pour être aussi restrictifs que possible à des fins mathématiques, mais le programmeur a de nombreuses façons de contourner cela si ce n'est pas le but, y compris l'arrondi vers le haut / vers le bas, la troncature, gt, lt, etc. . Les objets avec horodatage sont souvent comparés en fonction de leur temps de génération et chaque instance doit donc être unique pour que les comparaisons deviennent très spécifiques.

Le facteur de conception dans ce cas est de déterminer des moyens efficaces de comparer des objets. Parfois, une comparaison récursive de tous les objets membres de données est ce que vous devez faire, et cela peut devenir très coûteux si vous avez beaucoup, beaucoup d'objets avec beaucoup de membres de données. Les alternatives consistent à comparer uniquement les valeurs de données pertinentes, ou à faire générer par l'objet une valeur de hachage de ses membres de données concernés pour une comparaison rapide avec d'autres objets similaires, à conserver les collections triées et élaguées pour rendre les comparaisons plus rapides et moins gourmandes en CPU, et peut-être à autoriser les objets qui sont identiques dans les données à éliminer et un pointeur en double vers un seul objet soit mis à sa place.


2

La règle est, chaque fois que vous remplacez hashcode(), vous remplacez equals()et vice versa. Que ce soit une bonne idée ou non dépend de l'utilisation prévue. Personnellement, j'irais avec une méthode différente ( isLike()ou similaire) pour obtenir le même effet.


1
Il peut être correct de remplacer hashCode sans remplacer égal. Par exemple, on ferait cela pour tester un algorithme de hachage différent pour la même condition d'égalité.
Patricia Shanahan

1

Il est souvent utile pour les classes non publiques d'avoir une méthode de test d'équivalence qui permet aux objets de différents types de se considérer "égaux" s'ils représentent les mêmes informations, mais parce que Java ne permet aucun moyen par lequel les classes peuvent se faire passer pour chacune Dans d'autres cas, il est souvent bon d'avoir un seul type de wrapper ouvert au public dans les cas où il pourrait être possible d'avoir des objets équivalents avec des représentations différentes.

Par exemple, considérons une classe encapsulant une matrice 2D immuable de doublevaleurs. Si une méthode extérieure demande une matrice d'identité de taille 1000, une seconde demande une matrice diagonale et passe un tableau contenant 1000 unités, et une troisième demande une matrice 2D et passe un tableau 1000x1000 où les éléments de la diagonale principale sont tous 1.0 et tous les autres sont nuls, les objets donnés aux trois classes peuvent utiliser différents magasins de sauvegarde en interne [le premier ayant un seul champ pour la taille, le second ayant un tableau de mille éléments et le troisième ayant mille tableaux de 1000 éléments] mais devraient se rapporter comme équivalents [puisque les trois encapsulent une matrice immuable de 1000x1000 avec des sur la diagonale et des zéros partout ailleurs].

Au-delà du fait qu'il masque l'existence de types de stockage distincts, le wrapper sera également utile pour faciliter les comparaisons, car la vérification de l'équivalence des éléments sera généralement un processus en plusieurs étapes. Demandez au premier élément s'il sait s'il est égal au second; s'il ne sait pas, demandez au second s'il sait s'il est égal au premier. Si aucun objet ne le sait, demandez à chaque tableau le contenu de ses éléments individuels [on pourrait ajouter d'autres vérifications avant de décider de faire la route de comparaison individuelle longue-lente].

Notez que la méthode de test d'équivalence pour chaque objet de ce scénario devrait renvoyer une valeur à trois états ("Oui, je suis équivalent", "Non, je ne suis pas équivalent" ou "Je ne sais pas"), donc la méthode normale "égale" ne conviendrait pas. Alors que n'importe quel objet pourrait simplement répondre "Je ne sais pas" lorsqu'on lui en pose la question, ajouter une logique, par exemple à une matrice diagonale qui ne dérangerait pas de demander à une matrice d'identité ou à une matrice diagonale des éléments hors de la diagonale principale, accélérerait considérablement les comparaisons entre ces les types.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.