Apache Commons est égal à / générateur de hashCode [fermé]

155

Je suis curieux de savoir ce que les gens ici pensent d'utiliser org.apache.commons.lang.builder EqualsBuilder/ HashCodeBuilder pour implémenter le equals/ hashCode? Serait-ce une meilleure pratique que d'écrire la vôtre? Fonctionne-t-il bien avec Hibernate? Quelle est ton opinion?

aug70co
la source
16
Ne soyez pas tenté par les fonctions reflectionEqualset reflectionHashcode; la performance est un tueur absolu.
skaffman
14
J'ai vu des discussions ici sur les égaux hier et j'ai eu du temps libre, alors j'ai fait un test rapide. J'avais 4 objets avec différentes implémentations égales. eclipse générée, equalsbuilder.append, equalsbuilder.reflection et annotations pojomatiques. La ligne de base était l'éclipse. equalsbuilder.append a pris 3,7x. pojomatic a pris 5x. la réflexion basée a pris 25,8x. C'était assez décourageant car j'aime la simplicité de la réflexion basée et je ne supporte pas le nom "pojomatic".
digitaljoel
5
Une autre option est le projet Lombok; il utilise la génération de bytecode plutôt que la réflexion, il devrait donc fonctionner aussi bien que celui généré par Eclipse. projectlombok.org/features/EqualsAndHashCode.html
Miles

Réponses:

212

Les constructeurs commons / lang sont excellents et je les utilise depuis des années sans surcharge de performance notable (avec et sans mise en veille prolongée). Mais comme l'écrit Alain, la manière Goyave est encore plus agréable:

Voici un exemple de Bean:

public class Bean{

    private String name;
    private int length;
    private List<Bean> children;

}

Voici equals () et hashCode () implémentés avec Commons / Lang:

@Override
public int hashCode(){
    return new HashCodeBuilder()
        .append(name)
        .append(length)
        .append(children)
        .toHashCode();
}

@Override
public boolean equals(final Object obj){
    if(obj instanceof Bean){
        final Bean other = (Bean) obj;
        return new EqualsBuilder()
            .append(name, other.name)
            .append(length, other.length)
            .append(children, other.children)
            .isEquals();
    } else{
        return false;
    }
}

et ici avec Java 7 ou supérieur (inspiré de Guava):

@Override
public int hashCode(){
    return Objects.hash(name, length, children);
}

@Override
public boolean equals(final Object obj){
    if(obj instanceof Bean){
        final Bean other = (Bean) obj;
        return Objects.equals(name, other.name)
            && length == other.length // special handling for primitives
            && Objects.equals(children, other.children);
    } else{
        return false;
    }
}

Remarque: ce code faisait à l'origine référence à Guava, mais comme les commentaires l'ont souligné, cette fonctionnalité a depuis été introduite dans le JDK, donc Guava n'est plus nécessaire.

Comme vous pouvez le voir, la version Guava / JDK est plus courte et évite les objets auxiliaires superflus. En cas d'égalité, cela permet même de court-circuiter l'évaluation si un Object.equals()appel antérieur retourne false (pour être juste: commons / lang a une ObjectUtils.equals(obj1, obj2)méthode avec une sémantique identique qui pourrait être utilisée au lieu de EqualsBuilderpermettre un court-circuit comme ci-dessus).

Donc: oui, les communes constructeurs lang sont très préférables aux construits manuellement equals()et hashCode()méthodes (ou ces monstres terribles Eclipse va générer pour vous), mais les versions Java 7+ / Goyave sont encore mieux.

Et une note sur Hibernate:

soyez prudent lorsque vous utilisez des collections différées dans vos implémentations equals (), hashCode () et toString (). Cela échouera lamentablement si vous n'avez pas de session ouverte.


Remarque (environ égal à ()):

a) dans les deux versions de equals () ci-dessus, vous pouvez également utiliser l'un de ces raccourcis ou les deux:

@Override
public boolean equals(final Object obj){
    if(obj == this) return true;  // test for reference equality
    if(obj == null) return false; // test for null
    // continue as above

b) en fonction de votre interprétation du contrat equals (), vous pouvez également changer la (les) ligne (s)

    if(obj instanceof Bean){

à

    // make sure you run a null check before this
    if(obj.getClass() == getClass()){ 

Si vous utilisez la deuxième version, vous souhaiterez probablement également appeler super(equals())votre equals()méthode. Les opinions divergent ici, le sujet est discuté dans cette question:

bonne façon d'incorporer la superclasse dans une implémentation de Guava Objects.hashcode ()?

(même si c'est à peu près hashCode(), la même chose s'applique à equals())


Note (inspirée du commentaire de kayahr )

Objects.hashCode(..)(tout comme le sous-jacent Arrays.hashCode(...)) pourrait mal fonctionner si vous avez de nombreux champs primitifs. Dans de tels cas, EqualsBuilderpeut en fait être la meilleure solution.

Sean Patrick Floyd
la source
34
La même chose sera possible avec Java 7 Objects.equals: download.oracle.com/javase/7/docs/api/java/util/…
Thomas Jung
3
Si je le lis correctement, Josh Bloch dit dans Effective Java , Item 8, que vous ne devriez pas utiliser getClass () dans votre méthode equals (); vous devriez plutôt utiliser instanceof.
Jeff Olson
6
@SeanPatrickFloyd Le Guava-way crée non seulement un objet tableau pour les varargs, mais convertit également TOUS les paramètres en objets. Ainsi, lorsque vous lui passez 10 valeurs int, vous vous retrouvez avec 10 objets Integer et un objet tableau. La solution commons-lang ne crée qu'un seul objet, quel que soit le nombre de valeurs que vous ajoutez au code de hachage. Le même problème avec equals. Guava convertit toutes les valeurs en objets, commons-lang ne crée qu'un seul nouvel objet.
kayahr
1
@wonhee Je ne suis pas du tout d'accord pour dire que c'est mieux. Utiliser Reflection pour calculer les codes de hachage n'est jamais quelque chose que je ferais. La surcharge de performance est probablement négligeable, mais cela ne semble pas juste.
Sean Patrick Floyd
1
@kaushik faire une classe finale résout en fait les problèmes potentiels des deux versions (instanceof et getClass ()), tant que vous implémentez votre égal () dans les classes feuilles uniquement
Sean Patrick Floyd
18

Folks, réveillez-vous! Depuis Java 7, il existe des méthodes d'assistance pour equals et hashCode dans la bibliothèque standard. Leur utilisation est tout à fait équivalente à l'utilisation des méthodes Guava.

Mikhail Golubtsov
la source
a) au moment où cette question a été posée, Java 7 n'était pas encore là b) techniquement, ils ne sont pas tout à fait équivalents. jdk a la méthode Objects.equals par rapport aux méthodes Objects.equal de Guava. Je ne peux utiliser les importations statiques qu'avec la version de Guava. Ce ne sont que des cosmétiques, je sais, mais cela rend les non-goyaves nettement plus encombrés.
Sean Patrick Floyd
Ce n'est pas une bonne méthode pour surcharger une méthode objets equals car Objects.equals appellera la méthode .equals de l'instance. Si vous appelez Objects.equals dans la méthode .equals de l'instance, cela entraînera un débordement de pile.
dardo
Pouvez-vous donner un exemple, quand il tombe dans une boucle?
Mikhail Golubtsov
OP demande de remplacer la méthode equals () dans un objet. Selon la documentation de la méthode statique Objects.equals (): "Renvoie true si les arguments sont égaux entre eux et false dans le cas contraire. Par conséquent, si les deux arguments sont nuls, true est renvoyé et si exactement un argument est nul, false est renvoyé. Sinon, l'égalité est déterminée en utilisant la méthode equals du premier argument. "Par conséquent, si vous avez utilisé Objects.equals () dans l'instance remplacée equals (), il appellerait sa propre méthode equals, alors Objects.equals () puis lui-même à nouveau, donnant un débordement de pile.
dardo
@dardo Nous parlons d'implémenter l'égalité structurelle, donc cela signifie que deux objets sont égaux l'un à l'autre si leurs champs le font. Voir l'exemple de Guava ci-dessus, comment equals est implémenté.
Mikhail Golubtsov
8

Si vous ne voulez pas dépendre d'une bibliothèque tierce (peut-être que vous exécutez un appareil avec des ressources limitées) et que vous ne voulez même pas taper vos propres méthodes, vous pouvez également laisser l'EDI faire le travail, par exemple dans l'utilisation d'éclipse

Source -> Generate hashCode() and equals()...

Vous obtiendrez un code «natif» que vous pourrez configurer à votre guise et que vous devrez prendre en charge lors des modifications.


Exemple (éclipse Juno):

import java.util.Arrays;
import java.util.List;

public class FooBar {

    public String string;
    public List<String> stringList;
    public String[] stringArray;

    /* (non-Javadoc)
     * @see java.lang.Object#hashCode()
     */
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((string == null) ? 0 : string.hashCode());
        result = prime * result + Arrays.hashCode(stringArray);
        result = prime * result
                + ((stringList == null) ? 0 : stringList.hashCode());
        return result;
    }
    /* (non-Javadoc)
     * @see java.lang.Object#equals(java.lang.Object)
     */
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        FooBar other = (FooBar) obj;
        if (string == null) {
            if (other.string != null)
                return false;
        } else if (!string.equals(other.string))
            return false;
        if (!Arrays.equals(stringArray, other.stringArray))
            return false;
        if (stringList == null) {
            if (other.stringList != null)
                return false;
        } else if (!stringList.equals(other.stringList))
            return false;
        return true;
    }

}
FrVaBe
la source
14
C'est vrai, mais le code généré par Eclipse est illisible et impossible à maintenir.
Sean Patrick Floyd
6
S'il vous plaît, ne pensez jamais à quelque chose d'aussi terrible que l'éclipse générée equals. Si vous ne voulez pas dépendre d'une bibliothèque tierce, écrivez la méthode en une ligne comme Objects.equalvous. Même lorsqu'il n'est utilisé qu'une ou deux fois, le code est bien meilleur!
maaartinus
@maaartinus equals/ hashCodeméthodes d'une ligne ???
FrVaBe
1
@maaartinus Guava est une bibliothèque tierce. J'ai souligné que ma solution peut être utilisée si vous souhaitez éviter d'utiliser des bibliothèques tierces.
FrVaBe
1
@FrVaBe: Et j'ai écrit "Si vous ne voulez pas dépendre d'une bibliothèque tierce, écrivez la méthode en une ligne comme Objects.equal yourself." Et puis j'ai écrit la méthode en une ligne que vous pouvez utiliser pour ÉVITER d'utiliser Guava tout en réduisant la longueur de l'équivalent à environ la moitié.
maaartinus
6

EqualsBuilder et HashCodeBuilder ont deux aspects principaux qui sont différents du code écrit manuellement:

  • gestion nulle
  • création d'instance

EqualsBuilder et HashCodeBuilder facilitent la comparaison des champs qui pourraient être nuls. Avec le code écrit manuellement, cela crée beaucoup de passe-partout.

L'EqualsBuilder créera d'autre part une instance par appel de méthode equals. Si vos méthodes égales sont souvent appelées, cela créera de nombreuses instances.

Pour Hibernate, l'implémentation equals et hashCode ne font aucune différence. Ils ne sont qu'un détail de mise en œuvre. Pour presque tous les objets de domaine chargés avec hibernate, la surcharge d'exécution (même sans analyse d'échappement) du générateur peut être ignorée . Les frais généraux liés à la base de données et aux communications seront importants.

Comme skaffman l'a mentionné, la version de réflexion ne peut pas être utilisée dans le code de production. La réflexion sera trop lente et la "mise en œuvre" ne sera pas correcte pour toutes les classes sauf les plus simples. La prise en compte de tous les membres est également dangereuse car les membres nouvellement introduits modifient le comportement de la méthode égale. La version de réflexion peut être utile dans le code de test.

Thomas Jung
la source
Je ne suis pas d'accord pour dire que l'implémentation de la réflexion "ne sera pas correcte pour toutes les classes, sauf pour les classes les plus simples". Avec les générateurs, vous pouvez explicitement exclure des champs si vous le souhaitez, la mise en œuvre dépend donc vraiment de la définition de votre clé métier. Malheureusement, je ne peux pas être en désaccord avec l'aspect performance de l'implémentation basée sur la réflexion.
digitaljoel
1
@digitaljoel Oui, vous pouvez exclure des champs, mais ces définitions ne refactorisent pas. Je ne les ai donc pas mentionnés exprès.
Thomas Jung
0

Si vous vous occupez uniquement du bean entité où id est une clé primaire, vous pouvez simplifier.

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

      EntityBean castOther = (EntityBean) other;
      return new EqualsBuilder().append(this.getId(), castOther.getId()).isEquals();
   }
DEREK LEE
la source
0

À mon avis, cela ne fonctionne pas bien avec Hibernate, en particulier les exemples de la réponse comparant la longueur, le nom et les enfants d'une entité. Hibernate conseille d'utiliser la clé métier à utiliser dans equals () et hashCode (), et ils ont leurs raisons. Si vous utilisez le générateur auto equals () et hashCode () sur votre clé métier, ce n'est pas grave, seuls les problèmes de performances doivent être pris en compte comme mentionné précédemment. Mais les gens utilisent généralement toutes les propriétés, ce qui est très faux. Par exemple, je travaille actuellement sur un projet où les entités sont écrites en utilisant Pojomatic avec @AutoProperty, ce que je considère comme un très mauvais modèle.

Leurs deux principaux scénarios d'utilisation de hashCode () et equals () sont:

  • lorsque vous placez des instances de classes persistantes dans un ensemble (méthode recommandée pour représenter des associations à plusieurs valeurs) et
  • lorsque vous utilisez le rattachement d'instances détachées

Supposons donc que notre entité ressemble à ceci:

class Entity {
  protected Long id;
  protected String someProp;
  public Entity(Long id, String someProp);
}

Entity entity1 = new Entity(1, "a");
Entity entity2 = new Entity(1, "b");

Les deux sont la même entité pour Hibernate, qui ont été extraites d'une session à un moment donné (leur identifiant et leur classe / table sont égaux). Mais quand nous implémentons auto equals () un hashCode () sur tous les accessoires, qu'avons-nous?

  1. Lorsque vous placez l'entité2 dans l'ensemble persistant où l'entité1 existe déjà, cela sera placé deux fois et entraînera une exception lors de la validation.
  2. Si vous voulez attacher l'entité détachée2 à la session, là où entity1 existe déjà, elle (probablement, je n'ai pas testé cela en particulier) ne sera pas fusionnée correctement.

Donc, pour 99% du projet que je fais, nous utilisons l'implémentation suivante de equals () et hashCode () écrite une fois dans la classe d'entité de base, ce qui est cohérent avec les concepts Hibernate:

@Override
public boolean equals(Object obj) {
    if (StringUtils.isEmpty(id))
        return super.equals(obj);

    return getClass().isInstance(obj) && id.equals(((IDomain) obj).getId());
}

@Override
public int hashCode() {
    return StringUtils.isEmpty(id)
        ? super.hashCode()
        : String.format("%s/%s", getClass().getSimpleName(), getId()).hashCode();
}

Pour l'entité transitoire, je fais la même chose que Hibernate fera lors de l'étape de persistance, c'est-à-dire. J'utilise la correspondance d'instance. Pour les objets persistants, je compare la clé unique, qui est la table / id (je n'utilise jamais de clés composites).

Lukasz Frankowski
la source
0

Juste au cas où d'autres trouveraient cela utile, j'ai mis au point cette classe Helper pour le calcul de code de hachage qui évite la surcharge de création d'objets supplémentaire mentionnée ci-dessus (en fait, la surcharge de la méthode Objects.hash () est encore plus grande lorsque vous avez héritage car il créera un nouveau tableau à chaque niveau!).

Exemple d'utilisation:

public int hashCode() {
    return HashCode.hash(HashCode.hash(timestampMillis), name, dateOfBirth); // timestampMillis is long
}

public int hashCode() {
    return HashCode.hash(super.hashCode(), occupation, children);
}

L'assistant HashCode:

public class HashCode {

    public static int hash(Object o1, Object o2) {
        return add(Objects.hashCode(o1), o2);
    }

    public static int hash(Object o1, Object o2, Object o3) {
        return hash(Objects.hashCode(o1), o2, o3);
    }

    ...

    public static int hash(Object o1, Object o2, ..., Object o10) {
        return hash(Objects.hashCode(o1), o2, o3, ..., o10);
    }

    public static int hash(int initial, Object o1, Object o2) {
        return add(add(initial, o1), o2);
    }

    ...

    public static int hash(int initial, Object o1, Object o2, ... Object o10) {
        return add(... add(add(add(initial, o1), o2), o3) ..., o10);
    }

    public static int hash(long value) {
        return (int) (value ^ (value >>> 32));
    }

    public static int hash(int initial, long value) {
        return add(initial, hash(value));
    }

    private static int add(int accumulator, Object o) {
        return 31 * accumulator + Objects.hashCode(o);
    }
}

J'ai pensé que 10 est le nombre maximum raisonnable de propriétés dans un modèle de domaine, si vous en avez plus, vous devriez penser à refactoriser et à introduire plus de classes au lieu de maintenir un tas de chaînes et de primitives.

Les inconvénients sont: ce n'est pas utile si vous avez principalement des primitives et / ou des tableaux dont vous avez besoin pour hacher profondément. (Normalement, c'est le cas lorsque vous devez gérer des objets plats (transfert) qui sont hors de votre contrôle).

Vlad
la source