J'ai une classe abstraite qui remplace equals ()
et hashCode ()
. Dans ces méthodes, j'utilise la méthode abstraite getDescription ()
pour vérifier l'égalité et pour générer hashCode. Maintenant, quand j'étends la classe et que j'ajoute un champ qui n'est utilisé que dans la méthode getDescription ()
, j'obtiens un problème sonarLint "Étend une classe qui remplace equals et ajoute des champs". Est-ce juste que Sonar n'est pas assez sophistiqué pour comprendre ce qui se passe ou est-ce que je le fais de manière non java et il existe une méthode meilleure / plus élégante?
Classe parente:
public class Child extends Parent { private final String mMessage; public Child(final String message, final int number) { super(number); mMessage = message; } @Override public String getDescription() { return String.format( DESCRIPTION_FORMAT, mMessage); } }
Classe enfant:
public abstract String getDescription(); @Override public int hashCode() { return new HashCodeBuilder(19, 71). append(mViolation). append(getDescription()). append(mProperties). toHashCode(); } @Override public boolean equals( final Object obj) { boolean equal = false; if (this == obj) { equal = true; } else if (obj instanceof parent) { AbstractStructuredDataIssue rhs = (parent) obj; equal = new EqualsBuilder(). append(mViolation, rhs.mViolation). append(getDescription(), rhs.getDescription()). append(mProperties, rhs.mProperties). isEquals(); } return equal; }
4 Réponses :
C'est un peu compliqué; Je dois expliquer quelques choses sur la façon dont equals et hashCode fonctionnent pour expliquer les solutions viables.
Il existe un «contrat». Le compilateur ne peut pas l'appliquer, mais si vous n'adhérez pas à ce contrat, des choses étranges se produiront. Plus précisément: vos objets feront tout simplement la mauvaise chose lorsqu'ils sont utilisés comme clés dans les hashmaps, et éventuellement d'autres problèmes de ce type lors de l'utilisation de bibliothèques tierces. Pour adhérer correctement au contrat, toute classe donnée doit soit désactiver entièrement equals / hashCode, OU, toute la chaîne (donc, la classe et toutes ses sous-classes) doit remplacer correctement hashCode et equals, sauf que vous pouvez vraiment ' t faire cela à moins que le parent ne soit correctement instrumenté pour le faire.
Le contrat stipule que cela doit toujours être correct:
Le contrat est VRAIMENT difficile à garantir face à une hiérarchie de classes! Imaginez que nous prenions la java.util.ArrayList existante et la sous-classions avec la notion de «couleur». Nous pouvons maintenant avoir une ColoredArrayList bleue ou une ColoredArrayList rouge. Il serait parfaitement logique de dire qu'une ColoredArrayList bleue ne devrait certainement PAS être égale à une ColoredArrayList rouge, sauf que ... l'implémentation égale de ArrayList lui-même (que vous ne pouvez pas changer), définit effectivement que vous ne pouvez tout simplement pas étendre ArrayList avec des propriétés comme celle-ci du tout : si vous appelez a.equals (b) où a est une arraylist vide et b est une liste vide (disons, une ColoredArrayList rouge vide), cela vérifiera simplement l'égalité de chaque membre, ce qui , étant donné qu'ils sont tous les deux vides, est trivialement vrai. Ainsi, le tableau normal vide est égal à la fois au rouge vide et au bleu vide ColoredArrayList, et par conséquent, le contrat stipule qu'un ColoredArrayList rouge vide doit être égal à un ColoredArrayList bleu vide. En ce sens, le sonar est juste cassé ici. Il y a un problème et il ne peut pas être résolu. Il est impossible d'écrire le concept de ColoredArrayList en java .
Néanmoins, il existe une solution, mais seulement si toutes les classes de la hiérarchie sont à bord. C'est l'approche canEqual
. Le moyen de sortir du dilemme coloré ci-dessus est de différencier la notion de «j'étends et j'ajoute de nouvelles propriétés» et «j'étends, mais, ces choses sont toujours sémantiquement parlant exactement la même chose sans nouvelles propriétés». ColoredArrayList est le premier cas: c'est une extension qui ajoute de nouvelles propriétés. L'idée de canEqual est que vous créez une méthode distincte pour l'indiquer, ce qui permet à ArrayList de comprendre: je ne peux pas être égal à N'IMPORTE QUELLE occurrence de ColoredArrayList, même si tous les éléments sont identiques. Ensuite, nous pouvons à nouveau adhérer au contrat. ArrayList n'a PAS ce système en place et par conséquent, étant donné que vous ne pouvez pas changer le code source d'ArrayList, vous êtes bloqué: il n'est pas réparable. Mais si vous écrivez votre propre hiérarchie de classes, vous pouvez l'ajouter.
Project Lombok se charge d'ajouter des égaux et du hashCode pour vous. Même si vous ne souhaitez pas l'utiliser, vous pouvez regarder ce qu'il génère et le dupliquer dans votre propre code. Cela supprimera également les avertissements émis par le sonar. Voir https://projectlombok.org/features/EqualsAndHashCode - cela vous montre également comment le Le concept canEqual
peut être utilisé pour éviter le dilemme ColoredArrayList.
Ici, vous sous-classez sans ajouter de nouvelles propriétés, il n'est donc pas nécessaire de remplacer hashCode et equals. Mais le sonar ne le sait pas.
Jetons un œil à la règle RSPEC-2160 :
Étendez une classe qui remplace égal à égal et ajoutez des champs sans remplacer égale dans la sous-classe, et vous courez le risque de non-équivalent instances de votre sous-classe étant considérées comme égales , car seul le les champs de superclasse seront pris en compte dans le test d'égalité .
Ce que Sonar souligne, c'est le risque que des objets inégaux soient considérés comme égaux , puisque lorsque vous appelez equals
dans votre sous-classe, sans implémentation correcte, seuls les champs de la classe parent seront évalués.
Exemple de code non conforme (à partir de la documentation)
public class Fruit { private Season ripe; public boolean equals(Object obj) { if (obj == this) { return true; } if (this.class != obj.class) { return false; } Fruit fobj = (Fruit) obj; if (ripe.equals(fobj.getRipe()) { return true; } return false; } } public class Raspberry extends Fruit { private Color ripeColor; public boolean equals(Object obj) { if (! super.equals(obj)) { return false; } Raspberry fobj = (Raspberry) obj; if (ripeColor.equals(fobj.getRipeColor()) { // added fields are tested return true; } return false; } }
Le remplacement de la méthode equals () et hashcode () dans la classe enfant sera efficace en tenant compte des membres de la classe enfants (variables) et cela aide également en utilisant des sous-types de structure de collection et d'instances de mappage pour trouver espace mémoire droit (compartiment) pendant les opérations du framework de collecte (par exemple: enregistrer / récupérer).
L'héritage de la super classe ici peut manquer les membres de la classe enfants générer efficacement la fonctionnalité de méthode hashcode / equals.
Avec votre implémentation, vous pouvez avoir deux références Parent
qui sont égales , tout en pointant vers des objets de deux classes différentes, de sorte que l’une puisse être convertie en Enfant
et l'autre - pas.
Ceci est très inattendu et pourrait entraîner des problèmes sur la route - et c'est le travail de Sonar de le signaler. Si vous pensez que c'est justifié pour votre cas d'utilisation, documentez-le simplement en direct avec l'avertissement de Sonar (c'est ce que je ferais).