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

StackOverflow https://stackoverflow.com/questions/27581

Question

Quels problèmes/pièges doivent être pris en compte lors du remplacement equals et hashCode?

Était-ce utile?

La solution

La théorie (pour les juristes linguistiques et les mathématiciens enclins) :

equals() (javadoc) doit définir une relation d'équivalence (elle doit être réfléchi, symétrique, et transitif).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).

Le relation entre les deux méthodes est :

Chaque fois que 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() calculer hashCode().

Utilisez les excellentes classes d'assistance EqualsBuilder et HashCodeBuilder du Langage Apache Commons bibliothèque.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 :

Lors de l'utilisation d'un système basé sur le hachage Collection ou Carte tel que Jeu de hachage, LinkedHashSet, Carte de hachage, Table de hachage, ou Carte de hachage faible, assurez-vous que le hashCode() des objets clés que vous mettez dans la collection ne change jamais tant que l'objet est dans la collection.Le moyen infaillible de garantir cela est de rendre vos clés immuables, qui a aussi d'autres avantages.

Autres conseils

Il y a certains problèmes qui méritent d'être notés si vous avez affaire à des classes persistantes à l'aide d'un mappeur de relations 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 conservé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 quethis.getClass() == o.getClass() 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, en utilisant o instanceof Person est 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 des objets chargés paresseux.Cela signifie que person.name sera null si person est chargé paresseux, même si person.getName() force le chargement et renvoie "John Doe".D'après mon expérience, cela apparaît plus souvent dans hashCode() et equals().

Si vous utilisez un ORM, assurez-vous de toujours utiliser des getters et de ne jamais champ de références dans hashCode() et equals().

Enregistrer un objet changera son état

Les objets persistants utilisent souvent un id champ 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 d'identification dans hashCode().Mais vous pouvez l'utiliser dans equals().

Un modèle que j'utilise souvent est

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

Mais:tu ne peux pas inclure getId() dans hashCode().Si vous le faites, lorsqu'un objet est persistant, son hashCode changements.Si l'objet se trouve dans un HashSet, vous ne le retrouverez « jamais ».

Dans mon Person exemple, j'utiliserais probablement getName() pour hashCode et getId() plus getName() (juste pour la paranoïa) pour equals().Ce n'est pas grave s'il y a un risque de « collision » pour hashCode(), mais jamais d'accord pour equals().

hashCode() devrait utiliser le sous-ensemble non modifiable de propriétés de equals()

Une précision sur le obj.getClass() != getClass().

Cette déclaration est le résultat de equals() étant hostile à l'héritage.Le JLS (Spécification du langage Java) précise que si A.equals(B) == true alors B.equals(A) il faut aussi revenir true.Si vous omettez cette instruction, héritant des classes qui remplacent equals() (et changer son comportement) brisera cette spécification.

Prenons l'exemple suivant de ce qui se produit 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, new B(1,1).equals(new B(1,1)) le résultat donne vrai, comme il se doit.

Tout cela a l'air 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 souhaitez garantir la condition symétrique.a=b si b=a et l'appel du principe de substitution de Liskov super.equals(other) non seulement dans le cas de B par exemple, mais vérifiez après A exemple:

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;

Ce qui produira :

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

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

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

Résumé:

Dans son livre Guide efficace du langage de programmation Java (Addison-Wesley, 2001), Joshua Bloch affirme qu '"il n'y a tout simplement aucun moyen d'étendre une classe instanable 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 blindlyEquals() non symétrique dans les deux sens.blindlyEquals() est remplacé par les sous-classes, equals() est hérité et jamais remplacé.

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 c'est être satisfait.

Je suis toujours étonné que personne n'ait recommandé 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());
    }

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 obtenir plus, veuillez consulter ce lien comme 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

Amusez-vous!@.@

Il existe plusieurs façons de vérifier l'égalité des classes avant de vérifier l'égalité des membres, et je pense que les deux sont utiles dans les bonnes circonstances.

  1. Utilisez le instanceof opérateur.
  2. Utiliser this.getClass().equals(that.getClass()).

J'utilise le n°1 dans un final implémentation d'égal à égal, ou lors de l'implémentation d'une interface qui prescrit un algorithme pour égal (comme le java.util interfaces 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 les valeurs égales peuvent être remplacées car cela brise 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 est aussi Comparable, le equals et compareTo les méthodes doivent également être cohérentes.Voici un modèle pour la méthode égale dans un Comparable classe:

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;
  }

}

À égalité, examinez Secrets d'égaux par Angélique Langer.Je l'aime beaucoup.C'est aussi une excellente FAQ sur 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 !

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 on dit 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 cela peut être décidé en fonction de la définition de la classe et du contexte.Ensuite, la méthode equals() peut être remplacée.

nous devrions toujours remplacer la méthode hashCode() chaque fois que nous remplaçons 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 qui utilise l'adresse interne de l'objet, 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

Logiquement on a :

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

Mais pas vice versa!

Un problème que j'ai trouvé est que 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 vos tests hashCode ou égal, 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.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top