Remplacer la méthode java equals () - ne fonctionne pas?

150

Je suis tombé sur un problème intéressant (et très frustrant) avec la equals()méthode aujourd'hui, ce qui a provoqué le crash de ce que je pensais être une classe bien testée et un bug qui m'a pris beaucoup de temps à retrouver.

Juste pour être complet, je n'utilisais pas un IDE ou un débogueur - juste un bon éditeur de texte à l'ancienne et System.out. Le temps était très limité et c'était un projet scolaire.

De toute façon -

Je développais un panier de base qui pourrait contenir un ArrayListdes Bookobjets . Afin de mettre en œuvre les addBook(), removeBook()et les hasBook()méthodes du panier, je voulais vérifier si le Bookexistait déjà dans le Cart. Alors je pars -

public boolean equals(Book b) {
    ... // More code here - null checks
    if (b.getID() == this.getID()) return true;
    else return false;
}

Tout fonctionne bien dans les tests. Je crée 6 objets et les remplis de données. Faites beaucoup d'ajouts, de suppressions, d'opérations has () sur le Cartet tout fonctionne bien. J'ai lu que vous pouvez avoir equals(TYPE var)ouequals(Object o) { (CAST) var } mais supposer que puisque cela fonctionnait, cela n'avait pas trop d'importance.

Ensuite, j'ai rencontré un problème - je devais créer un Bookobjet avec uniquement le IDcontenu de la classe Book. Aucune autre donnée n'y serait introduite. Fondamentalement, ce qui suit:

public boolean hasBook(int i) {
    Book b = new Book(i);
    return hasBook(b);
}

public boolean hasBook(Book b) {
    // .. more code here
    return this.books.contains(b);
}

Du coup, la equals(Book b)méthode ne fonctionne plus. Cela a pris TRÈS longtemps à traquer sans un bon débogueur et en supposant que la Cartclasse était correctement testée et correcte. Après avoir permuté la equals()méthode à la suivante:

public boolean equals(Object o) {
    Book b = (Book) o;
    ... // The rest goes here   
}

Tout a recommencé à fonctionner. Y a-t-il une raison pour laquelle la méthode a décidé de ne pas prendre le paramètre Book alors qu'il s'agissait clairement d' un Bookobjet? La seule différence semblait être qu'elle était instanciée à partir de la même classe et remplie uniquement avec un membre de données. Je suis très très confus. S'il vous plaît, faites la lumière?

Josh Smeaton
la source
1
Je suis conscient que j'ai violé le `` contrat '' concernant le remplacement des méthodes égales en étant réfléchi - cependant, j'avais besoin d'un moyen rapide de vérifier si l'objet existait dans l'ArrayList sans utiliser de génériques.
Josh Smeaton
1
C'est une bonne leçon à apprendre sur Java et ses égaux
jjnguy

Réponses:

329

En Java, la equals()méthode héritée Objectest:

public boolean equals(Object other);

En d'autres termes, le paramètre doit être de type Object. C'est ce qu'on appelle le remplacement ; votre méthode public boolean equals(Book other)fait ce qu'on appelle une surcharge de la equals()méthode.

Le ArrayListutilise des equals()méthodes surchargées pour comparer le contenu (par exemple pour ses méthodes contains()et equals()), pas celles surchargées. Dans la plupart de votre code, appeler celui qui ne remplaçait pas correctement Objectles égaux était correct , mais pas compatible avec ArrayList.

Ainsi, ne pas remplacer correctement la méthode peut causer des problèmes.

Je remplace à chaque fois ce qui suit:

@Override
public boolean equals(Object other){
    if (other == null) return false;
    if (other == this) return true;
    if (!(other instanceof MyClass)) return false;
    MyClass otherMyClass = (MyClass)other;
    ...test other properties here...
}

L'utilisation de l' @Overrideannotation peut aider une tonne d'erreurs stupides.

Utilisez-le chaque fois que vous pensez remplacer la méthode d'une super classe ou d'une interface. De cette façon, si vous ne le faites pas correctement, vous obtiendrez une erreur de compilation.

jjnguy
la source
31
C'est un bon argument en faveur de l'annotation @Override ... si l'OP avait utilisé @Override, son compilateur lui aurait dit qu'il ne remplaçait pas réellement une méthode de classe parente ...
Cowan
1
Je n'ai jamais été au courant du @Override, merci pour cela! Je voudrais également ajouter que le remplacement de hashCode () aurait vraiment dû être fait et aurait peut-être détecté l'erreur plus tôt.
Josh Smeaton
5
Certains IDE (par exemple Eclipse) peuvent même générer automatiquement les méthodes equals () et hashcode () pour vous en fonction des variables des membres de la classe.
sk.
1
if (!(other instanceof MyClass))return false;retourne falsesi MyClassétend l'autre classe. Mais il ne reviendrait pas falsesi l'autre classe s'étendait MyClass. Ne devrait pas equalêtre moins contradictoire?
Robert
19
Lors de l'utilisation de l'instance, le contrôle nul précédent est redondant.
Mateusz Dymczyk
108

Si vous utilisez eclipse, allez simplement dans le menu du haut

Source -> Générer equals () et hashCode ()

Fred
la source
Je suis d'accord! Celui-ci que je n'avais jamais connu auparavant et le générer le rend moins sujet aux erreurs
Garçon
Pareil ici. Merci Fred!
Anila
16
Dans IntelliJ, vous trouvez cela sous Code → Générer… ou contrôle + N. :)
rightfold
Dans Netbeans, vous allez dans la barre de menu> Source (ou clic droit)> Insérer le code (ou Ctrl-I), et cliquez sur Générer égal () ...
Solomon
11

Légèrement hors sujet par rapport à votre question, mais cela vaut probablement la peine de le mentionner quand même:

Commons Lang a d'excellentes méthodes que vous pouvez utiliser pour remplacer les égaux et le hashcode. Découvrez EqualsBuilder.reflectionEquals (...) et HashCodeBuilder.reflectionHashCode (...) . M'a sauvé beaucoup de maux de tête dans le passé - bien que, bien sûr, si vous voulez juste faire «égal» sur ID, cela peut ne pas correspondre à votre situation.

Je suis également d'accord pour que vous utilisiez l' @Overrideannotation chaque fois que vous remplacez égal (ou toute autre méthode).


la source
4
Si vous êtes un utilisateur d'éclipse, vous pouvez également y aller right click -> source -> generate hashCode() and equals(),
tunaranch
1
Ai-je raison de dire que cette méthode est exécutée au moment de l'exécution? N'aurons-nous pas des problèmes de performances au cas où nous traverserions une grande collection avec des éléments vérifiant leur égalité avec un autre élément en raison de la réflexion?
Gaket le
4

Une autre solution rapide qui enregistre le code standard est l'annotation Lombok EqualsAndHashCode . C'est simple, élégant et personnalisable. Et ne dépend pas de l'IDE . Par exemple;

import lombok.EqualsAndHashCode;

@EqualsAndHashCode(of={"errorNumber","messageCode"}) // Will only use this fields to generate equals.
public class ErrorMessage{

    private long        errorNumber;
    private int         numberOfParameters;
    private Level       loggingLevel;
    private String      messageCode;

Voir les options disponibles pour personnaliser les champs à utiliser dans les égaux. Lombok est disponible en maven . Ajoutez-le simplement avec la portée fournie :

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.14.8</version>
    <scope>provided</scope>
</dependency>
Borjab
la source
1

dans Android Studio est alt + insert ---> equals et hashCode

Exemple:

    @Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;

    Proveedor proveedor = (Proveedor) o;

    return getId() == proveedor.getId();

}

@Override
public int hashCode() {
    return getId();
}
David Hackro
la source
1

Considérer:

Object obj = new Book();
obj.equals("hi");
// Oh noes! What happens now? Can't call it with a String that isn't a Book...
bcsb1001
la source
1
@Elazar Comment ça? objest déclaré comme un Object. Le point d'héritage est que vous pouvez ensuite attribuer un Bookà obj. Après cela, à moins que vous ne suggériez qu'un Objectne soit pas comparable à un Stringvia equals(), ce code doit être parfaitement légal et revenir false.
bcsb1001
Je suggère exactement cela. Je pense que c'est assez largement accepté.
Elazar
0

l' instanceOfinstruction est souvent utilisée dans l'implémentation d'égaux.

C'est un piège populaire!

Le problème est que l'utilisation instanceOfviole la règle de symétrie:

(object1.equals(object2) == true) si et seulement si (object2.equals(object1))

si le premier égal est vrai et que object2 est une instance d'une sous-classe de la classe à laquelle obj1 appartient, alors le second égal retournera faux!

si la classe considérée à laquelle appartient ob1 est déclarée comme finale, alors ce problème ne peut pas se poser, mais en général, vous devez tester comme suit:

this.getClass() != otherObject.getClass(); sinon, retournez false, sinon testez les champs à comparer pour l'égalité!

Nikel8000
la source
3
Voir Bloch, Effective Java, Item 8, une grande section traitant des problèmes liés au remplacement de la equals()méthode. Il recommande de ne pas utiliser getClass(). La raison principale est que cela enfreint le principe de substitution de Liskov pour les sous-classes qui n'affectent pas l'égalité.
Stuart marque le
-1

recordId est la propriété de l'objet

@Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Nai_record other = (Nai_record) obj;
        if (recordId == null) {
            if (other.recordId != null)
                return false;
        } else if (!recordId.equals(other.recordId))
            return false;
        return true;
    }
vootla561
la source