Quels problèmes doivent être pris en compte lors de la substitution de equals et hashCode en Java?

617

Quels problèmes / pièges doivent être pris en compte lors de la substitution equalset hashCode?

Matt Sheppard
la source

Réponses:

1439

La théorie (pour les juristes du langage et les inclinés mathématiquement):

equals()( javadoc ) doit définir une relation d'équivalence (elle doit être réflexive , symétrique et transitive ). De plus, il doit être cohérent (si les objets ne sont pas modifiés, alors il doit continuer à renvoyer la même valeur). En outre, o.equals(null)doit toujours retourner faux.

hashCode()( javadoc ) doit également être cohérent (si l'objet n'est pas modifié en termes de equals(), il doit continuer à renvoyer la même valeur).

La relation entre les deux méthodes est:

Chaque fois a.equals(b), alors a.hashCode()doit être le même que b.hashCode().

En pratique:

Si vous remplacez l'un, vous devez remplacer l'autre.

Utilisez le même ensemble de champs que vous utilisez pour calculer equals()pour calculer hashCode().

Utilisez les excellentes classes d'assistance EqualsBuilder et HashCodeBuilder de la bibliothèque Apache Commons Lang . Un exemple:

public class Person {
    private String name;
    private int age;
    // ...

    @Override
    public int hashCode() {
        return new HashCodeBuilder(17, 31). // two randomly chosen prime numbers
            // if deriving: appendSuper(super.hashCode()).
            append(name).
            append(age).
            toHashCode();
    }

    @Override
    public boolean equals(Object obj) {
       if (!(obj instanceof Person))
            return false;
        if (obj == this)
            return true;

        Person rhs = (Person) obj;
        return new EqualsBuilder().
            // if deriving: appendSuper(super.equals(obj)).
            append(name, rhs.name).
            append(age, rhs.age).
            isEquals();
    }
}

N'oubliez pas non plus:

Lorsque vous utilisez une collection ou une carte de hachage telle que HashSet , LinkedHashSet , HashMap , Hashtable ou WeakHashMap , assurez-vous que le hashCode () des objets clés que vous placez dans la collection ne change jamais tant que l'objet se trouve dans la collection. La manière à l'épreuve des balles de garantir cela est de rendre vos clés immuables, ce qui présente également d'autres avantages .

Antti Sykäri
la source
12
Point supplémentaire sur appendSuper (): vous devez l'utiliser dans hashCode () et equals () si et seulement si vous voulez hériter du comportement d'égalité de la superclasse. Par exemple, si vous dérivez directement d'Object, cela ne sert à rien car tous les Objects sont distincts par défaut.
Antti Kissaniemi
312
Vous pouvez demander à Eclipse de générer les deux méthodes pour vous: Source> Générer hashCode () et equals ().
Rok Strniša
27
Il en va de même avec Netbeans: developmentality.wordpress.com/2010/08/24/…
seinecle
6
@Darthenius Eclipse généré égal à égal utilise getClass () qui peut causer des problèmes dans certains cas (voir l'article Java effectif 8)
AndroidGecko
7
La première vérification null n'est pas nécessaire étant donné que instanceofrenvoie false si son premier opérande est null (Java effectif à nouveau).
izaban
295

Il y a quelques problèmes à noter si vous traitez avec des classes qui persistent avec un mappeur de relation d'objet (ORM) comme Hibernate, si vous ne pensiez pas que c'était déjà déraisonnablement compliqué!

Les objets chargés paresseux sont des sous-classes

Si vos objets sont persistés à l'aide d'un ORM, dans de nombreux cas, vous aurez affaire à des proxys dynamiques pour éviter de charger l'objet trop tôt à partir du magasin de données. Ces proxys sont implémentés en tant que sous-classes de votre propre classe. Cela signifie que this.getClass() == o.getClass()cela reviendra false. Par exemple:

Person saved = new Person("John Doe");
Long key = dao.save(saved);
dao.flush();
Person retrieved = dao.retrieve(key);
saved.getClass().equals(retrieved.getClass()); // Will return false if Person is loaded lazy

Si vous avez affaire à un ORM, l'utilisation o instanceof Personest la seule chose qui se comportera correctement.

Les objets chargés paresseux ont des champs nuls

Les ORM utilisent généralement les getters pour forcer le chargement d'objets chargés paresseux. Cela signifie que ce person.namesera nullsi personest chargé paresseusement, même si person.getName()force le chargement et renvoie "John Doe". D'après mon expérience, cela revient plus souvent dans hashCode()et equals().

Si vous avez affaire à un ORM, assurez-vous de toujours utiliser des getters et de ne jamais mettre en référence des références dans hashCode()et equals().

L'enregistrement d'un objet changera son état

Les objets persistants utilisent souvent un idchamp pour contenir la clé de l'objet. Ce champ sera automatiquement mis à jour lors de la première sauvegarde d'un objet. N'utilisez pas de champ id dans hashCode(). Mais vous pouvez l'utiliser dans equals().

Un motif que j'utilise souvent est

if (this.getId() == null) {
    return this == other;
}
else {
    return this.getId().equals(other.getId());
}

Mais vous ne pouvez pas inclure getId()dans hashCode(). Si vous le faites, lorsqu'un objet persiste, ses hashCodemodifications. Si l'objet est dans un HashSet, vous ne le retrouverez "plus" jamais.

Dans mon Personexemple, j'utiliserais probablement getName()pour hashCodeet getId()plus getName()(juste pour la paranoïa) pour equals(). Ce n'est pas grave s'il y a un risque de "collisions" hashCode(), mais jamais correct equals().

hashCode() doit utiliser le sous-ensemble de propriétés inchangé de equals()

Johannes Brodwall
la source
2
@Johannes Brodwall: je ne comprends pas Saving an object will change it's state! hashCodedoit revenir int, alors comment allez-vous utiliser getName()? Pouvez-vous donner un exemple pour votrehashCode
jimmybondy
@jimmybondy: getName retournera un objet String qui a également un hashCode qui peut être utilisé
mateusz.fiolka
85

Une clarification sur le obj.getClass() != getClass().

Cette déclaration est le résultat d' equals()un héritage hostile. Le JLS (spécification du langage Java) spécifie que si A.equals(B) == truealors B.equals(A)doit également retourner true. Si vous omettez cette instruction héritant des classes qui remplacent equals()(et modifient son comportement), cette spécification sera rompue.

Prenons l'exemple suivant de ce qui se passe lorsque l'instruction est omise:

    class A {
      int field1;

      A(int field1) {
        this.field1 = field1;
      }

      public boolean equals(Object other) {
        return (other != null && other instanceof A && ((A) other).field1 == field1);
      }
    }

    class B extends A {
        int field2;

        B(int field1, int field2) {
            super(field1);
            this.field2 = field2;
        }

        public boolean equals(Object other) {
            return (other != null && other instanceof B && ((B)other).field2 == field2 && super.equals(other));
        }
    }    

Faire new A(1).equals(new A(1))aussi, le new B(1,1).equals(new B(1,1))résultat donne vrai, comme il se doit.

Cela semble très bien, mais regardez ce qui se passe si nous essayons d'utiliser les deux classes:

A a = new A(1);
B b = new B(1,1);
a.equals(b) == true;
b.equals(a) == false;

De toute évidence, c'est faux.

Si vous voulez assurer la condition symétrique. a = b si b = a et le principe de substitution de Liskov appellent super.equals(other)non seulement dans le cas de l' Binstance, mais vérifient après par Aexemple:

if (other instanceof B )
   return (other != null && ((B)other).field2 == field2 && super.equals(other)); 
if (other instanceof A) return super.equals(other); 
   else return false;

Qui produira:

a.equals(b) == true;
b.equals(a) == true;

Où, si ce an'est pas une référence de B, alors ce pourrait être une référence de classe A(parce que vous l'étendez), dans ce cas vous appelez super.equals() aussi .

Ran Biron
la source
2
Vous pouvez rendre les égaux symétriques de cette façon (si vous comparez un objet de superclasse avec un objet de sous-classe, utilisez toujours les égaux de la sous-classe) if (obj.getClass ()! = This.getClass () && obj.getClass (). IsInstance (this) ) retourne obj.equals (this);
pihentagy
5
@pihentagy - alors j'obtiendrais un stackoverflow lorsque la classe d'implémentation ne remplace pas la méthode equals. pas drôle.
Ran Biron
2
Vous n'obtiendrez pas un stackoverflow. Si la méthode equals n'est pas remplacée, vous appellerez à nouveau le même code, mais la condition de récursivité sera toujours fausse!
Jacob Raihle
@pihentagy: Comment cela se comporte-t-il s'il existe deux classes dérivées différentes? Si a ThingWithOptionSetApeut être égal à a à Thingcondition que toutes les options supplémentaires aient des valeurs par défaut, et de même pour a ThingWithOptionSetB, alors il devrait être possible pour a ThingWithOptionSetAd'être comparé égal à a ThingWithOptionSetBseulement si toutes les propriétés non-base des deux objets correspondent à leurs valeurs par défaut, mais Je ne vois pas comment vous testez cela.
supercat
7
Le problème avec cela, c'est qu'il rompt la transitivité. Si vous ajoutez B b2 = new B(1,99), puis b.equals(a) == trueet a.equals(b2) == truemais b.equals(b2) == false.
nickgrim
46

Pour une implémentation respectueuse de l'héritage, consultez la solution de Tal Cohen, Comment puis-je implémenter correctement la méthode equals ()?

Sommaire:

Dans son livre Effective Java Programming Language Guide (Addison-Wesley, 2001), Joshua Bloch affirme qu '"il n'y a tout simplement aucun moyen d'étendre une classe instanciable et d'ajouter un aspect tout en préservant le contrat égal". Tal n'est pas d'accord.

Sa solution consiste à implémenter equals () en appelant un autre aveuglément non symétrique Equals () dans les deux sens. blindlyEquals () est écrasé par les sous-classes, equals () est hérité et n'est jamais écrasé.

Exemple:

class Point {
    private int x;
    private int y;
    protected boolean blindlyEquals(Object o) {
        if (!(o instanceof Point))
            return false;
        Point p = (Point)o;
        return (p.x == this.x && p.y == this.y);
    }
    public boolean equals(Object o) {
        return (this.blindlyEquals(o) && o.blindlyEquals(this));
    }
}

class ColorPoint extends Point {
    private Color c;
    protected boolean blindlyEquals(Object o) {
        if (!(o instanceof ColorPoint))
            return false;
        ColorPoint cp = (ColorPoint)o;
        return (super.blindlyEquals(cp) && 
        cp.color == this.color);
    }
}

Notez que equals () doit fonctionner à travers les hiérarchies d'héritage si le principe de substitution de Liskov doit être satisfait.

Kevin Wong
la source
10
Jetez un œil à la méthode canEqual expliquée ici - le même principe fait fonctionner les deux solutions, mais avec canEqual vous ne comparez pas deux fois les mêmes champs (ci-dessus, px == this.x sera testé dans les deux sens): artima.com /lejava/articles/equality.html
Blaisorblade
2
En tout cas, je ne pense pas que ce soit une bonne idée. Cela rend le contrat Equals déroutant inutilement - quelqu'un qui prend deux paramètres Point, a et b, doit être conscient de la possibilité que a.getX () == b.getX () et a.getY () == b.getY () peut être vrai, mais a.equals (b) et b.equals (a) sont tous les deux faux (si un seul est un ColorPoint).
Kevin
Fondamentalement, c'est comme if (this.getClass() != o.getClass()) return false, mais flexible en ce qu'il ne renvoie false que si la ou les classes dérivées prennent la peine de modifier égal. Est-ce correct?
Aleksandr Dubinsky
31

Toujours étonné qu'aucun ne recommande la bibliothèque de goyave pour cela.

 //Sample taken from a current working project of mine just to illustrate the idea

    @Override
    public int hashCode(){
        return Objects.hashCode(this.getDate(), this.datePattern);
    }

    @Override
    public boolean equals(Object obj){
        if ( ! obj instanceof DateAndPattern ) {
            return false;
        }
        return Objects.equal(((DateAndPattern)obj).getDate(), this.getDate())
                && Objects.equal(((DateAndPattern)obj).getDate(), this.getDatePattern());
    }
Eugène
la source
23
java.util.Objects.hash () et java.util.Objects.equals () font partie de Java 7 (publié en 2011), vous n'avez donc pas besoin de Guava pour cela.
herman
1
bien sûr, mais vous devez éviter cela car Oracle ne fournit plus de mises à jour publiques pour Java 6 (ce qui est le cas depuis février 2013).
herman
6
Votre thisau this.getDate()moyen de rien (autre que le désordre)
Steve Kuo
1
Votre « pas instanceof » expression a besoin d' un support supplémentaire: if (!(otherObject instanceof DateAndPattern)) {. D'accord avec hernan et Steve Kuo (bien que ce soit une question de préférence personnelle), mais +1 néanmoins.
Amos M. Carpenter
26

Il existe deux méthodes dans la super classe comme java.lang.Object. Nous devons les remplacer par un objet personnalisé.

public boolean equals(Object obj)
public int hashCode()

Les objets égaux doivent produire le même code de hachage tant qu'ils sont égaux, mais les objets inégaux n'ont pas besoin de produire des codes de hachage distincts.

public class Test
{
    private int num;
    private String data;
    public boolean equals(Object obj)
    {
        if(this == obj)
            return true;
        if((obj == null) || (obj.getClass() != this.getClass()))
            return false;
        // object must be Test at this point
        Test test = (Test)obj;
        return num == test.num &&
        (data == test.data || (data != null && data.equals(test.data)));
    }

    public int hashCode()
    {
        int hash = 7;
        hash = 31 * hash + num;
        hash = 31 * hash + (null == data ? 0 : data.hashCode());
        return hash;
    }

    // other methods
}

Si vous voulez en savoir plus, veuillez consulter ce lien sous http://www.javaranch.com/journal/2002/10/equalhash.html

Ceci est un autre exemple, http://java67.blogspot.com/2013/04/example-of-overriding-equals-hashcode-compareTo-java-method.html

S'amuser! @. @

Luna Kong
la source
Désolé mais je ne comprends pas cette déclaration sur la méthode hashCode: elle n'est pas légale si elle utilise plus de variables que equals (). Mais si je code avec plus de variables, mon code se compile. Pourquoi n'est-ce pas légal?
Adryr83
19

Il y a deux façons de vérifier votre égalité de classe avant de vérifier l'égalité des membres, et je pense que les deux sont utiles dans les bonnes circonstances.

  1. Utilisez l' instanceofopérateur.
  2. Utilisez this.getClass().equals(that.getClass()).

J'utilise # 1 dans une finalimplémentation égale, ou lors de l'implémentation d'une interface qui prescrit un algorithme pour égal (comme les java.utilinterfaces de collecte - la bonne façon de vérifier avec (obj instanceof Set)ou quelle que soit l'interface que vous implémentez). C'est généralement un mauvais choix lorsque des égaux peuvent être remplacés car cela rompt la propriété de symétrie.

L'option n ° 2 permet d'étendre la classe en toute sécurité sans remplacer les égaux ni rompre la symétrie.

Si votre classe l'est également Comparable, les méthodes equalset compareTodoivent également être cohérentes. Voici un modèle pour la méthode equals dans une Comparableclasse:

final class MyClass implements Comparable<MyClass>
{

  

  @Override
  public boolean equals(Object obj)
  {
    /* If compareTo and equals aren't final, we should check with getClass instead. */
    if (!(obj instanceof MyClass)) 
      return false;
    return compareTo((MyClass) obj) == 0;
  }

}
erickson
la source
1
+1 pour cela. Ni getClass () ni instanceof ne sont une panacée, et c'est une bonne explication de la façon d'aborder les deux. Ne pensez pas qu'il y ait une raison de ne pas faire this.getClass () == that.getClass () au lieu d'utiliser equals ().
Paul Cantrell
Il y a un problème avec cela. Les classes anonymes qui n'ajoutent aucun aspect et ne remplacent pas la méthode equals échoueront la vérification getClass même si elles doivent être égales.
steinybot
@Steiny Il n'est pas clair pour moi que les objets de différents types doivent être égaux; Je pense aux différentes implémentations d'une interface comme une classe anonyme commune. Pouvez-vous donner un exemple pour soutenir votre prémisse?
erickson
MyClass a = nouveau MyClass (123); MyClass b = new MyClass (123) {// Remplacer une méthode}; // a.equals (b) est faux lors de l'utilisation de this.getClass (). equals (that.getClass ())
steinybot
1
@Steiny Right. Comme il se doit dans la plupart des cas, surtout si une méthode est remplacée au lieu d'être ajoutée. Considérez mon exemple ci-dessus. Si ce n'était pas le cas finalet que la compareTo()méthode a été remplacée pour inverser l'ordre de tri, les instances de la sous-classe et de la superclasse ne devraient pas être considérées comme égales. Lorsque ces objets étaient utilisés ensemble dans une arborescence, les clés qui étaient «égales» selon une instanceofimplémentation pouvaient ne pas être trouvées.
erickson
16

Pour des égaux, regardez dans Secrets of Equals par Angelika Langer . Je l'aime beaucoup. Elle est également une excellente FAQ sur les génériques en Java . Voir ses autres articles ici (faites défiler jusqu'à "Core Java"), où elle continue également avec la partie 2 et la "comparaison de types mixtes". Amusez-vous à les lire!

Johannes Schaub - litb
la source
11

La méthode equals () est utilisée pour déterminer l'égalité de deux objets.

car la valeur int de 10 est toujours égale à 10. Mais cette méthode equals () concerne l'égalité de deux objets. Quand nous disons objet, il aura des propriétés. Pour décider de l'égalité, ces propriétés sont prises en compte. Il n'est pas nécessaire que toutes les propriétés soient prises en compte pour déterminer l'égalité et en ce qui concerne la définition de classe et le contexte, il peut être décidé. Ensuite, la méthode equals () peut être remplacée.

nous devons toujours remplacer la méthode hashCode () chaque fois que nous substituons la méthode equals (). Sinon, que se passera-t-il? Si nous utilisons des tables de hachage dans notre application, elle ne se comportera pas comme prévu. Comme le hashCode est utilisé pour déterminer l'égalité des valeurs stockées, il ne renverra pas la bonne valeur correspondante pour une clé.

L'implémentation par défaut donnée est la méthode hashCode () dans la classe Object utilise l'adresse interne de l'objet et la convertit en entier et la renvoie.

public class Tiger {
  private String color;
  private String stripePattern;
  private int height;

  @Override
  public boolean equals(Object object) {
    boolean result = false;
    if (object == null || object.getClass() != getClass()) {
      result = false;
    } else {
      Tiger tiger = (Tiger) object;
      if (this.color == tiger.getColor()
          && this.stripePattern == tiger.getStripePattern()) {
        result = true;
      }
    }
    return result;
  }

  // just omitted null checks
  @Override
  public int hashCode() {
    int hash = 3;
    hash = 7 * hash + this.color.hashCode();
    hash = 7 * hash + this.stripePattern.hashCode();
    return hash;
  }

  public static void main(String args[]) {
    Tiger bengalTiger1 = new Tiger("Yellow", "Dense", 3);
    Tiger bengalTiger2 = new Tiger("Yellow", "Dense", 2);
    Tiger siberianTiger = new Tiger("White", "Sparse", 4);
    System.out.println("bengalTiger1 and bengalTiger2: "
        + bengalTiger1.equals(bengalTiger2));
    System.out.println("bengalTiger1 and siberianTiger: "
        + bengalTiger1.equals(siberianTiger));

    System.out.println("bengalTiger1 hashCode: " + bengalTiger1.hashCode());
    System.out.println("bengalTiger2 hashCode: " + bengalTiger2.hashCode());
    System.out.println("siberianTiger hashCode: "
        + siberianTiger.hashCode());
  }

  public String getColor() {
    return color;
  }

  public String getStripePattern() {
    return stripePattern;
  }

  public Tiger(String color, String stripePattern, int height) {
    this.color = color;
    this.stripePattern = stripePattern;
    this.height = height;

  }
}

Exemple de sortie de code:

bengalTiger1 and bengalTiger2: true 
bengalTiger1 and siberianTiger: false 
bengalTiger1 hashCode: 1398212510 
bengalTiger2 hashCode: 1398212510 
siberianTiger hashCode: 1227465966
rohan kamat
la source
7

Logiquement, nous avons:

a.getClass().equals(b.getClass()) && a.equals(b)a.hashCode() == b.hashCode()

Mais pas vice-versa!

Khaled.K
la source
6

Un problème que j'ai trouvé est celui où deux objets contiennent des références l'un à l'autre (un exemple étant une relation parent / enfant avec une méthode pratique sur le parent pour obtenir tous les enfants).
Ce genre de choses est assez courant lors des mappages Hibernate par exemple.

Si vous incluez les deux extrémités de la relation dans votre hashCode ou des tests égaux, il est possible d'entrer dans une boucle récursive qui se termine par une StackOverflowException.
La solution la plus simple consiste à ne pas inclure la collection getChildren dans les méthodes.

Darren Greaves
la source
5
Je pense que la théorie sous-jacente ici est de faire la distinction entre les attributs , les agrégats et les associatinos d'un objet. Les associations ne devraient pas participer equals(). Si un savant fou créait une copie de moi, nous serions équivalents. Mais nous n'aurions pas le même père.
Raedwald