web-dev-qa-db-fra.com

Est-ce une bonne idée d'avoir une logique dans la méthode des égaux qui ne fait pas de correspondance exacte?

Tout en aidant un étudiant dans un projet universitaire, nous avons travaillé sur un Java exercice fourni par l'université qui a défini une classe pour une adresse avec les champs:

number
street
city
zipcode

Et il a spécifié que la logique égale doit retourner true si le nombre et le code postal correspondent.

On m'a appris une fois que la méthode des égaux ne devrait faire qu'une comparaison exacte entre les objets (après avoir vérifié le pointeur), ce qui est logique pour moi, mais en contradiction avec la tâche qui leur a été confiée.

Je peux voir pourquoi vous voudriez remplacer la logique afin que vous puissiez utiliser des choses comme list.contains() avec votre correspondance partielle, mais je me demande si cela est considéré comme casher, et sinon pourquoi?

35
William Dunne

Définition de l'égalité pour deux objets

L'égalité peut être définie arbitrairement pour deux objets quelconques. Il n'y a pas de règle stricte qui interdit à quelqu'un de définir comme il l'entend. Cependant, l'égalité est souvent définie lorsqu'elle est significative pour les règles de domaine de ce qui est mis en œuvre.

Il devrait suivre le contrat de relation d'équivalence :

  • C'est réflexif : pour toute valeur de référence non nulle x, x.equals (x) doit retourner vrai.
  • Il est symétrique : pour toutes les valeurs de référence non nulles x et y, x.equals (y) doit retourner vrai si et seulement si y.equals ( x) renvoie vrai.
  • C'est transitif : pour toutes les valeurs de référence non nulles x, y et z, si x.equals (y) renvoie true et y.equals ( z) renvoie vrai, alors x.equals (z) doit retourner vrai.
  • Il est cohérent : pour toutes les valeurs de référence non nulles x et y, plusieurs invocations de x.equals (y) retournent systématiquement true ou retournent systématiquement false, à condition qu'aucune information utilisée dans des comparaisons égales sur les objets ne soit modifiée.
  • Pour toute valeur de référence non nulle x, x.equals (null) doit renvoyer false.

Dans votre exemple, il n'est peut-être pas nécessaire de distinguer deux adresses qui ont le même code postal et le même numéro comme étant différentes. Il existe des domaines qui sont parfaitement raisonnables de s'attendre à ce que le code suivant fonctionne:

Address a1 = new Address("123","000000-0","Street Name","City Name");
Address a2 = new Address("123","000000-0","Str33t N4me","C1ty N4me");
assert a1.equals(a2);

Cela peut être utile, comme vous l'avez mentionné, lorsque vous ne vous souciez pas qu'ils soient des objets différents - vous ne vous souciez que des valeurs qu'ils contiennent. Peut-être que le code postal + le numéro de rue vous suffisent pour identifier la bonne adresse et les informations restantes sont "supplémentaires", et vous ne voulez pas que ces informations supplémentaires affectent votre logique d'égalité.

Cela pourrait être une très bonne modélisation pour un logiciel. Assurez-vous simplement qu'il existe de la documentation ou des tests unitaires pour garantir ce comportement et que l'API publique reflète cette utilisation.


N'oubliez pas hashCode()

Un détail supplémentaire pertinent pour l'implémentation est le fait que de nombreuses langues utilisent fortement le concept de code de hachage . Ces langages, Java y compris, supposent généralement la proposition suivante:

Si x.equals (y), alors x.hashCode () et y.hashCode () sont identiques.

A partir du même lien que précédemment:

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

Notez que le fait d'avoir le même hashCode ne signifie pas que deux objets sont égaux !

En ce sens, quand on implémente l'égalité, il faut également implémenter une hashCode() qui suit la propriété mentionnée ci-dessus. Cette hashCode() est utilisée par les structures de données pour l'efficacité et la garantie de limites supérieures sur la complexité de leurs opérations.

Trouver une bonne fonction de code de hachage est difficile et un sujet entier sur lui-même. Idéalement, le hashCode de deux objets différents devrait être différent ou avoir une distribution uniforme entre les occurrences d'instance.

Mais gardez à l'esprit que l'implémentation simple suivante remplit toujours la propriété d'égalité, même si ce n'est pas une "bonne" fonction de hachage:

public int hashCode() {
    return 0;
}

Une façon plus courante d'implémenter du code de hachage consiste à utiliser les codes de hachage des champs qui définissent votre égalité et à effectuer une opération binaire sur ceux-ci. Dans votre exemple, code postal et numéro de rue. Cela se fait souvent comme:

public int hashCode() {
    return this.zipCode.hashCode() ^ this.streetNumber.hashCode();
}

En cas d'ambiguïté, choisissez la clarté

C'est ici que je fais une distinction sur ce à quoi il faut s'attendre en matière d'égalité. Différentes personnes ont des attentes différentes en matière d'égalité et si vous cherchez à suivre le Principe du moindre étonnement vous pouvez envisager d'autres options pour mieux décrire votre conception.

Lequel de ceux-ci devrait être considéré comme égal?

Address a1 = new Address("123","000000-0","Street Name","City Name");
Address a2 = new Address("123","000000-0","Str33t N4me","C1ty N4me");
assert a1.equals(a2); // Are typos the same address?
Address a1 = new Address("123","000000-0","John Street","SpringField");
Address a2 = new Address("123","000000-0","John St.","SpringField");
assert a1.equals(a2); // Are abbreviations the same address?
Vector3 v1 = new Vector3(1.0f, 1.0f, 1.0f);
Vector3 v2 = new Vector3(1.0f, 1.0f, 1.0f);
assert v1.equals(v2); // Should two vectors that have the same values be the same?
Vector3 v1 = new Vector3(1.00000001f, 1.0f, 1.0f);
Vector3 v2 = new Vector3(1.0f, 1.0f, 1.0f);
assert v1.equals(v2); // What is the error tolerance?

Un cas pourrait être fait pour chacun de ceux qui sont vrais ou faux. En cas de doute, on peut définir une relation différente qui est plus claire dans le contexte du domaine.

Par exemple, vous pouvez définir isSameLocation(Address a):

Address a1 = new Address("123","000000-0","John Street","SpringField");
Address a2 = new Address("123","000000-0","John St.","SpringField");

System.out.print(a1.equals(a2)); // false;
System.out.print(a1.isSameLocation(a2)); // true;

Ou dans le cas des vecteurs, isInRangeOf(Vector v, float range):

Vector3 v1 = new Vector3(1.000001f, 1.0f, 1.0f);
Vector3 v2 = new Vector3(1.0f, 1.0f, 1.0f);

System.out.print(v1.equals(v2)); // false;
System.out.print(v1.isInRangeOf(v2, 0.01f)); // true;

De cette façon, vous décrivez mieux votre intention de conception pour l'égalité et vous évitez de briser les attentes des futurs lecteurs concernant ce que fait réellement votre code. (Vous pouvez simplement jeter un coup d'œil à toutes les réponses légèrement différentes pour voir comment les attentes des gens varient en ce qui concerne la relation d'égalité de votre exemple)

89
Albuquerque

C'est dans le contexte de la mission universitaire que le but de la tâche est d'explorer et de comprendre la priorité de l'opérateur. Cela ressemble à un exemple de tâche qui a suffisamment d'objectifs implicites pour le faire apparaître comme un exercice valable à l'époque.

Cependant, si c'était une revue de code par moi, je marquerais cela comme un défaut de conception important.

Le problème est le suivant. Il permet un code syntaxiquement propre qui semble évidemment correct:

if (driverLocation.equals(parcel.deliveryAddress)) { parcel.deliver(); }

Et sur la base des commentaires d'autres utilisateurs, ce code produirait des résultats corrects au Brésil où les codes postaux sont uniques à une rue. Cependant, si vous avez ensuite essayé d'utiliser ce logiciel aux États-Unis où cette hypothèse n'est plus valide, ce code semble toujours correct.

si cela avait été mis en œuvre comme:

if (Address.isMatchNumberAndZipcode(driverLocation, parcel.deliveryAddress)) {
  parcel.deliver();
}

puis quelques années plus tard, lorsqu'un développeur brésilien différent reçoit la base de code et lui dit que le logiciel livre des colis aux mauvaises adresses pour leur nouveau client en Californie, l'hypothèse désormais brisée est évidente dans le code et est visible au point de décision sur s'il faut livrer ou non - ce qui est probablement le premier endroit que le programmeur de maintenance examine pour voir pourquoi le colis est livré à la mauvaise adresse.

Le fait d'avoir une logique non évidente cachée dans une surcharge de l'opérateur rendra la correction du code plus longue. Pour attraper ce problème dans ce code, il faudrait probablement une session avec un débogueur parcourant le code.

42
Michael Shaw

L'égalité est une question de contexte. La question de savoir si deux objets sont considérés comme égaux est autant une question de contexte que des deux objets impliqués.

Donc, si dans votre contexte, il est logique d'ignorer la ville et la rue, alors il n'y a aucun problème à implémenter l'égalité uniquement basée sur le code postal et le numéro. (Comme cela a été souligné dans l'un des commentaires, le code postal et le numéro sont suffisants pour identifier de manière unique une adresse au Brésil.)

Bien sûr, vous devez vous assurer de suivre les règles appropriées pour surcharger l'égalité, par exemple en vous assurant de surcharger hashCode en conséquence.

25
Jörg W Mittag

Un opérateur d'égalité prétendra que deux objets sont égaux si et seulement s'ils doivent être considérés comme égaux, en raison des considérations que vous jugez utiles.

Je le répète: pour toutes les considérations que vous jugez utiles.

Le développeur du logiciel est aux commandes ici. En plus d'être cohérent avec des exigences évidentes (a = a, a = b implique b + a, a = b et b = c implique a = c) et la cohérence avec la fonction de hachage) l'opérateur d'égalité peut être ce que vous voulez.

3
gnasher729

Bien que de nombreuses réponses aient été données, mon opinion n'est toujours pas présente.

On m'a appris une fois que la méthode des égaux ne devrait faire qu'une comparaison exacte entre les objets

En dehors de ce que disent les règles, cette définition est ce que les gens supposent de leur intiution quand ils parlent de égalité. Certaines réponses disent que l'égalité dépend du contexte. Ils ont raison en ce sens que les objets peuvent être égaux même si tous leurs champs ne correspondent pas. Mais la compréhension commune de "est égal" ne doit pas être trop redéfinie.

Revenons au sujet, pour moi une adresse égale à une autre si elle pointe vers le même emplacement.

En Allemagne, il peut y avoir différentes spécifications d'une ville, par exemple si une banlieue est nommée. Ensuite, la ville d'une adresse dans la banlieue SUB peut être indiquée comme "ville principale" seulement ou "ville principale, SUB" ou même seulement "SUB". Parce que donner le nom de la ville principale est correct, tous les noms de rue dans une ville et toutes les banlieues qui lui sont attribuées doivent être uniques.

Ici, le code postal suffit pour indiquer la ville, même si le nom de la ville varie.
Mais quitter la rue n'est PAS unique, à moins que le code postal pointe également vers une rue bien connue, ce qui n'est généralement pas le cas.
Il n'est donc pas intuitif de considérer deux adresses égales si elles peuvent pointer vers des emplacements différents dont la différence est constituée des champs ignorés.

S'il existe un cas d'utilisation ne nécessitant que certains mais tous les champs, la méthode de comparaison doit être nommée de manière appropriée. Il n'y a qu'une seule méthode "est égal" qui ne devrait pas être secrètement transformée en "est égale pour un seul cas d'utilisation spécial - mais personne ne peut le voir".

Cela signifie, pour les raisons expliquées, je dirais ...

mais je me demande si cela est considéré comme casher

Sans savoir si vous vous trouvez accidentellement dans un endroit où les noms de rue n'ont pas d'importance: non, ce n'est pas le cas.
Si vous voulez programmer quelque chose non seulement utilisé dans un tel endroit: non, ce n'est pas le cas.
Si vous voulez donner aux élèves le sentiment de bien faire les choses et de garder le code compréhensible et logique: non, ce n'est pas le cas.

2
puck

Bien que l'exigence donnée contredit le sens humain il est OK de ne laisser qu'un sous-ensemble des propriétés des objets définir le sens de "unique".

Le problème ici est qu'il existe une relation technique entre equals() et hashcode() de sorte que pour deux objets a et b de ce type est réputé être :
if a.equals(b) then a.hashcode()==b.hashcode()
Si vous disposez d'un sous-ensemble des propriétés définissant vos conditions d'unicité, vous devez utiliser le même sous-ensemble pour calculer la valeur de retour de hashcode().

Après tout, l'approche beaucoup plus appropriée pour l'exigence peut avoir été d'implémenter Comparable ou même une méthode isSame() personnalisée.

1
Timothy Truckle

Cela dépend.

Est-ce une bonne idée ...? Cela dépend. Cela peut être une bonne idée, si vous développez une application qui ne sera utilisée qu'une seule fois , par exemple, dans une mission univercity (si vous allez pour jeter le code après examen de l'affectation), ou un utilitaire de migration (vous migrez les données héritées une fois et vous n'avez plus besoin de l'utilitaire).

Mais dans l'industrie informatique, dans de nombreux cas, ce serait une mauvaise idée. Pourquoi? @ Jörg W Mittag a dit L'égalité est une question de contexte ... si dans votre contexte cela a du sens ... . Mais souvent, le même objet est utilisé dans de nombreux contextes différents qui ont différents point de vue sur l'égalité. Juste quelques exemples de la façon dont on peut définir différemment l'égalité d'une même entité:

  • Comme égalité de tous les attributs de deux entités
  • Comme égalité des clés primaires de deux entités
  • Comme égalité des clés primaires et des versions de deux entités
  • Comme égalité de tous les attributs "métier" sauf la clé primaire et la version

Si vous implémentez dans equals () la logique pour un contexte particulier, il sera difficile plus tard d'utiliser cet objet dans d'autres contextes, car de nombreux développeurs dans les équipes de votre projet ne sauront pas exactement la logique pour quel contexte exactement est implémenté là-bas et dans quels cas ils peuvent s'y fier. Dans certains cas, ils l'utiliseront de manière incorrecte (comme décrit @Michael Shaw), dans d'autres cas, ils ignoreront la logique et mettront en œuvre leurs propres méthodes dans le même but (qui peuvent fonctionner différemment de ce que vous attendiez).

Si votre application va être utilisée pendant une plus longue période comme 2-3 ans, il y aura normalement plusieurs nouvelles exigences, plusieurs changements et plusieurs contextes. Et très probablement, il y aura de multiples attentes différentes en matière d'égalité. C'est pourquoi je suggère:

  • Implémenter equals () formellement, sans connexion au contexte métier, signifie sans aucune logique métier, tout comme l'égalité de tous les attributs d'objet (bien sûr hashCode/equals contrat doit être respecté)
  • Pour chaque contexte, fournissez une méthode distincte qui implémente l'égalité au sens de ce contexte, comme isPrimaryKeyAndVersionEqual () , areBusinessAttributesEqual () .

Ensuite, pour trouver un objet dans un contexte particulier, vous utilisez simplement la méthode correspondante, comme suit:

if (list.sream.anyMatch(e -> e.isPrimaryKeyAndVersionEqual(myElement))) ...

if (list.sream.anyMatch(e -> e.areBusinessAttributesEqual(myElement))) ...

Ainsi, il y aura moins de bogues dans le code, l'analyse du code sera plus facile, le changement de l'application pour de nouvelles exigences sera plus facile.

1
mentallurg

Comme d'autres l'ont mentionné, d'une part, l'égalité n'est qu'un concept mathématique satisfaisant certaines propriétés (voir par exemple Albuquerque's réponse). D'autre part, sa sémantique et sa mise en œuvre sont déterminées par le contexte.

Indépendamment des détails d'implémentation, prenez par exemple une classe représentant des expressions arithmétiques (comme (1 + 3) * 5). Si vous implémentez un interpréteur pour de telles expressions en utilisant les règles d'évaluation standard pour les expressions arithmétiques, il est logique de considérer les instances respectives de (1 + 3) * 5 et 10 + 10 être equal. Cependant, si vous implémentez une jolie imprimante pour de telles expressions ci-dessus, les instances ne seraient pas considérées comme equal, tandis que (1 + 3) * 5 et (1+3)*5 aurait.

0
michid

Comme d'autres l'ont mentionné, la sémantique exacte de l'égalité des objets fait partie de la définition du domaine métier. Dans ce cas, je ne pense pas qu'il soit raisonnable d'avoir un objet "général" comme Address (contenant number, street, city, zipcode) à une définition très étroite de l'égalité (qui, comme d'autres l'ont mentionné, fonctionne au Brésil mais pas aux États-Unis, par exemple).

Au lieu de cela, j'aurais Address une sémantique de valeur pour l'égalité (définie par l'égalité de tous les membres). Je voudrais alors soit:

  1. Créez une classe StreeNumberAndZip (# TODO: bad name), Qui contient uniquement un street et un zipCode, et définit equals par-dessus. Chaque fois que vous voulez comparer deux objets Address de cette manière particulière, vous pouvez faire addressA.streetNumberAndZip().equals(addressB.streetNumberAndZip()), ou ...
  2. Créez une classe AddressUtils avec une méthode bool equalStreeNumberAndZipCode(Address a, Address b), qui y définit l'égalité étroite.

Dans les deux cas, vous pouvez toujours utiliser addressA.equals(addressB) pour une vérification complète de l'égalité.

Pour les champs n d'un objet, il existe 2^n Différentes définitions d'égalité (chaque champ peut être inclus ou exclu de la vérification). Si vous devez vérifier l'égalité de différentes manières, il peut également être utile d'avoir quelque chose comme un enum AddressComponent. Vous pouvez alors avoir une bool addressComponentsAreEqual(EnumSet<AddressComponent> equatedComponents, Address a, Address b), vous pouvez donc appeler quelque chose comme

bool addressAreKindOfEqual = AddressUtils.addressComponentsAreEqual(
    new EnumSet.of(
        AddressComponent.streetNumber, 
        AddressComponent.zipCode,
    ),
    addressA, addressB
);

C'est évidemment beaucoup plus typé, mais cela peut vous éviter d'avoir une explosion exponentielle de méthodes de vérification d'égalité.

L'égalité est subtile pour aller de l'avant et son importance est d'une portée trompeuse. Surtout dans les langues où l'implémentation d'un opérateur d'égalité signifie soudainement que votre objet est censé jouer Nice avec des ensembles et des cartes.

Dans l'immense majorité des cas, l'égalité doit être une identité, ce qui signifie qu'un objet est égal à un autre si et seulement s'il est le même morceau de mémoire avec le même adresse. La relation d'identité respecte toujours toutes les conditions d'une relation d'égalité appropriée: réflexivité, transitivité, etc. L'identité est également le moyen le plus rapide de comparer deux choses, car vous comparez simplement les deux pointeurs. Le respect des contrats de relation d'équivalence est la chose la plus importante dans toute mise en œuvre de l'égalité, car le non-respect de cette règle se traduit par des bogues notoirement difficiles à diagnostiquer.

La deuxième façon d'implémenter égal est de comparer si les types correspondent, puis de comparer chaque champ "possédé" de l'objet. Cela finit souvent par revenir dans les détails de chaque objet. Si votre objet entre dans des structures de données qui appellent égal, égal sera probablement ce que la structure de données passe la plupart de son temps à faire si vous utilisez cette approche. Il y a d'autres problèmes:

  • si l'objet change, le résultat de sa comparaison avec d'autres objets change également, ce qui casse toutes sortes d'hypothèses que les classes standard font sur l'égalité;
  • si votre objet est dans une hiérarchie de classe/interface, la seule façon saine de comparer deux objets dans cette hiérarchie est si leurs types concrets correspondent exactement (voir l'excellent Java de Joshua Bloch Java efficace réservez pour plus de détails à ce sujet);
  • si vous essayez de rendre la relation d'égalité très stricte en incluant autant de champs que possible, vous finirez par vous retrouver dans une situation où votre égalité ne correspond pas à une logique métier de "similitude".

La troisième façon serait de sélectionner uniquement les champs qui sont pertinents pour la logique métier et d'ignorer le reste. La probabilité que cette approche soit rompue est arbitrairement proche de 1. La première raison mentionnée par d'autres est qu'une comparaison qui a du sens dans un contexte ne fait pas '' t nécessairement sens dans tous les contextes . Le langage vous demande de définir une égalité de forme, afin que cela fonctionne mieux dans tous les contextes. Pour les adresses, une telle logique de comparaison n'existe tout simplement pas. Vous pouvez avoir des méthodes spécialisées "ces deux adresses identiques identiques" mais vous ne devriez pas risquer qu'une telle méthode soit la seule vraie façon de comparer en tant que cela déroutera inévitablement les lecteurs.

Je recommanderais également de jeter un coup d'œil aux programmeurs de Falsehoods qui croient aux adresses: https://www.mjt.me.uk/posts/falsehoods-programmers-believe-about-addresses/ c'est une lecture amusante et pourrait vous aider à éviter certains écueils.

0
Kafein