web-dev-qa-db-fra.com

Style d'utilisation de HashCodeBuilder et EqualsBuilder

J'utilise souvent Apache HashCodeBuilder et EqualsBuilder pour l'égalité des objets à l'aide de la réflexion, mais récemment, un collègue m'a dit que l'utilisation de la réflexion peut entraîner une baisse considérable des performances si l'entité contient de nombreuses propriétés. Inquiet que j'utilise peut-être une mauvaise mise en œuvre, ma question est la suivante: laquelle des approches suivantes préférez-vous? Et pourquoi?

public class Admin {

    private Long id;
    private String userName;

    public String getUserName() {
        return userName;
    }

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof Admin)) {
            return false;
        }
        Admin otherAdmin  = (Admin) o;
        EqualsBuilder builder = new EqualsBuilder();
        builder.append(getUserName(), otherAdmin.getUserName());
        return builder.isEquals();
    }

    @Override
    public int hashCode() {
        HashCodeBuilder builder = new HashCodeBuilder();
        builder.append(getUserName());
        return builder.hashCode();
    }
}

Vs.

public class Admin {

    private Long id;
    private String userName;

    public String getUserName() {
        return userName;
    }

    @Override
    public boolean equals(Object o) {
      return EqualsBuilder.reflectionEquals(this, o, Arrays.asList(id));
    }

    @Override
    public int hashCode() {
        return HashCodeBuilder.reflectionHashCode(this, Arrays.asList(id));
    }
}
28
tintin

Bien sûr, la deuxième option est plus élégante et simple. Mais si vous êtes préoccupé par les performances, vous devriez opter pour la première approche, la deuxième méthode échoue également si un gestionnaire de sécurité est en cours d'exécution.

J'irai pour la première option si j'étais dans votre situation. Il y a aussi une erreur dans votre première approche lors de la génération de hashCode, ce devrait être return builder.toHashCode (); au lieu de renvoyer builder.hashCode (); (qui renvoie les objets de générateur de hachage code de hachage)

19
ssk

Même si la deuxième option est plus attrayante (car il ne s'agit que d'une seule ligne de code), je choisirais la première option.

La raison en est simplement la performance. Après avoir effectué un petit test, j'ai trouvé une très grande différence de temps entre eux.

Afin de me faire une idée du temps, j'ai créé ces deux classes simples:

package equalsbuildertest;

import Java.math.BigDecimal;
import Java.util.Date;

import org.Apache.commons.lang3.builder.EqualsBuilder;
import org.Apache.commons.lang3.builder.HashCodeBuilder;

public class Class1 {

    private int field1;

    private boolean field2;

    private BigDecimal field3;

    private String field4;

    private Date field5;

    private long field6;

    public Class1(int field1, boolean field2, BigDecimal field3, String field4,
            Date field5, long field6) {
        super();
        this.field1 = field1;
        this.field2 = field2;
        this.field3 = field3;
        this.field4 = field4;
        this.field5 = field5;
        this.field6 = field6;
    }

    public Class1() {
        super();
    }

    public int getField1() {
        return field1;
    }

    public void setField1(int field1) {
        this.field1 = field1;
    }

    public boolean isField2() {
        return field2;
    }

    public void setField2(boolean field2) {
        this.field2 = field2;
    }

    public BigDecimal getField3() {
        return field3;
    }

    public void setField3(BigDecimal field3) {
        this.field3 = field3;
    }

    public String getField4() {
        return field4;
    }

    public void setField4(String field4) {
        this.field4 = field4;
    }

    public Date getField5() {
        return field5;
    }

    public void setField5(Date field5) {
        this.field5 = field5;
    }

    public long getField6() {
        return field6;
    }

    public void setField6(long field6) {
        this.field6 = field6;
    }

    @Override
    public boolean equals(Object o) {
      return EqualsBuilder.reflectionEquals(this, o);
    }

    @Override
    public int hashCode() {
        return HashCodeBuilder.reflectionHashCode(this);
    }

}

Et:

package equalsbuildertest;

import Java.math.BigDecimal;
import Java.util.Date;

import org.Apache.commons.lang3.builder.EqualsBuilder;
import org.Apache.commons.lang3.builder.HashCodeBuilder;

public class Class2 {

    private int field1;

    private boolean field2;

    private BigDecimal field3;

    private String field4;

    private Date field5;

    private long field6;

    public Class2(int field1, boolean field2, BigDecimal field3, String field4,
            Date field5, long field6) {
        super();
        this.field1 = field1;
        this.field2 = field2;
        this.field3 = field3;
        this.field4 = field4;
        this.field5 = field5;
        this.field6 = field6;
    }

    public Class2() {
        super();
    }

    public int getField1() {
        return field1;
    }

    public void setField1(int field1) {
        this.field1 = field1;
    }

    public boolean isField2() {
        return field2;
    }

    public void setField2(boolean field2) {
        this.field2 = field2;
    }

    public BigDecimal getField3() {
        return field3;
    }

    public void setField3(BigDecimal field3) {
        this.field3 = field3;
    }

    public String getField4() {
        return field4;
    }

    public void setField4(String field4) {
        this.field4 = field4;
    }

    public Date getField5() {
        return field5;
    }

    public void setField5(Date field5) {
        this.field5 = field5;
    }

    public long getField6() {
        return field6;
    }

    public void setField6(long field6) {
        this.field6 = field6;
    }

    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof Class2)) {
            return false;
        }
        Class2 other = (Class2) obj;
        EqualsBuilder builder = new EqualsBuilder();
        builder.append(field1, other.field1);
        builder.append(field2, other.field2);
        builder.append(field3, other.field3);
        builder.append(field4, other.field4);
        builder.append(field5, other.field5);
        builder.append(field6, other.field6);
        return builder.isEquals();
    }

    @Override
    public int hashCode() {
        HashCodeBuilder builder = new HashCodeBuilder();
        builder.append(getField1());
        builder.append(isField2());
        builder.append(getField3());
        builder.append(getField4());
        builder.append(getField5());
        builder.append(getField6());
        return builder.hashCode();

    };

}

Cette deuxième classe est à peu près la même que la première, mais avec différents égaux et hashCode.

Après cela, j'ai créé les tests suivants:

package equalsbuildertest;

import static org.junit.Assert.*;

import Java.math.BigDecimal;
import Java.util.Date;

import org.junit.Test;

public class EqualsBuilderTest {

    @Test
    public void test1() {
        Class1 class1a = new Class1(1, true, new BigDecimal(0), "String", new Date(), 1L);
        Class1 class1b = new Class1(1, true, new BigDecimal(0), "String", new Date(), 1L);
        for (int i = 0; i < 1000000; i++) {
            assertEquals(class1a, class1b);
        }
    }

    @Test
    public void test2() {
        Class2 class2a = new Class2(1, true, new BigDecimal(0), "String", new Date(), 1L);
        Class2 class2b = new Class2(1, true, new BigDecimal(0), "String", new Date(), 1L);
        for (int i = 0; i < 1000000; i++) {
            assertEquals(class2a, class2b);
        }
    }

}

Les tests sont assez simples et ne servent qu'à mesurer le temps.

Les résultats sont les suivants:

  • test1 (2 024 s)
  • test2 (0,039 s)

Je les ai choisis pour être complètement égaux afin d'avoir les meilleurs moments. Si vous choisissez de faire le test avec les conditions NotEquals, vous aurez un temps plus court, mais en gardant également un très grand décalage horaire.

J'exécute ces tests sur un processeur Intel Core i5-3317U 64 bits à 1,70 GHz x4 avec Fedora 21 et Eclipse Luna.

En conclusion, je ne risquerais pas une si grande différence de performances afin d'économiser quelques lignes de code que vous ne pouvez peut-être pas taper de toute façon en utilisant un modèle (dans Eclipse sous Windows -> Les préférences se trouvent dans Java -> Editeur -> Modèles) comme celui-ci:

${:import(org.Apache.commons.lang3.builder.HashCodeBuilder, org.Apache.commons.lang3.builder.EqualsBuilder)}
@Override
public int hashCode() {
    HashCodeBuilder hashCodeBuilder = new HashCodeBuilder();
    hashCodeBuilder.append(${field1:field});
    hashCodeBuilder.append(${field2:field});
    hashCodeBuilder.append(${field3:field});
    hashCodeBuilder.append(${field4:field});
    hashCodeBuilder.append(${field5:field});
    return hashCodeBuilder.toHashCode();
}

@Override
public boolean equals(Object obj) {
    if (this == obj) {
        return true;
    }
    if (obj == null) {
        return false;
    }
    if (getClass() != obj.getClass()) {
        return false;
    }
    ${enclosing_type} rhs = (${enclosing_type}) obj;
    EqualsBuilder equalsBuilder = new EqualsBuilder();
    equalsBuilder.append(${field1}, rhs.${field1});
    equalsBuilder.append(${field2}, rhs.${field2});
    equalsBuilder.append(${field3}, rhs.${field3});
    equalsBuilder.append(${field4}, rhs.${field4});
    equalsBuilder.append(${field5}, rhs.${field5});${cursor}
    return equalsBuilder.isEquals();
}
14
Seba D'Agostino

Je préférerais la deuxième option pour 2 raisons:

  1. De toute évidence, il est plus facile à lire

  2. Je n'achèterais pas d'arguments de performance pour la première option, à moins que ceux-ci incluent une métrique pertinente. Par exemple. combien de millisecondes des "égaux" basés sur la réflexion ajouteraient-ils à une latence de demande de bout en bout typique? Dans l'ensemble, quel serait le pourcentage d'augmentation? Sans savoir que de bonnes chances sont que l'optimisation est prématurée

8
Pavel Grushetzky

Je dirais que ni l'un ni l'autre n'est une bonne mise en œuvre. Je dirais que EqualsBuilder n'est pas un bon cadre à utiliser pour les raisons suivantes:

  1. Non extensible. Que faire si l'un des champs dans lesquels vous essayez d'affirmer l'égalité doit traiter les valeurs nulles et vides comme égales?
  2. Vous devez conserver la liste des variables comme s'il s'agissait de variables codées en dur. Cela signifie que vous devez lister toutes les variables que vous souhaitez comparer. À ce stade, il n'y a pas de différence entre a == o.getA () b == o.getB () ...
  3. L'utilisation de la réflexion prend des ressources supplémentaires, comme vous l'avez souligné, et dans une application d'entreprise qui écrase des milliards d'objets. Faire cette réflexion égale est aussi mauvais que d'avoir une fuite de mémoire.

Je dirai qu'il doit y avoir un meilleur cadre que celui d'Apache.

3
Churk

Votre question telle qu'elle est rédigée illustre clairement l'un des avantages de la deuxième approche:

Dans le premier cas, il est très facile de faire l'erreur return builder.hashCode(), au lieu de la bonne return builder.toHashCode(), ce qui entraînera des erreurs subtiles qui peuvent être très difficiles pour traquer.

Le deuxième cas élimine la possibilité de cette faute de frappe, ce qui entraîne moins de se cogner la tête dans le clavier en essayant de trouver le bogue.

3
Krease

D'accord avec @Churk, Apache HashCodeBuilder et EqualsBuilder ne sont pas bien implémentés. HashCodeBuilder joue toujours avec des nombres premiers! De plus, il fait énormément de travail inutile. Avez-vous lu la source?

Depuis Java 5 (sinon plus tôt), AbstractHashMap <> n'a pas utilisé de module d'un nombre premier pour localiser le compartiment de hachage. Au lieu de cela, le nombre de compartiments est une puissance de deux et l'ordre inférieur N bits du code de hachage sont utilisés pour localiser le compartiment.

De plus, il "mélangera" le code de hachage fourni par l'application de sorte que les bits soient répartis uniformément et, ainsi, les compartiments soient remplis uniformément.

Ainsi, la bonne façon de remplacer int Object.hashCode () est de renvoyer la valeur constante la plus simple avec l'arité la plus élevée dans la population d'objets qui cohabiteront dans toute collection en utilisant la classe.

En règle générale, la valeur de l'ID non modifié est votre meilleur pari. Si votre champ ID est intégral, il vous suffit de le convertir en (int) et de le renvoyer. S'il s'agit d'une chaîne ou d'un autre objet, renvoyez simplement son code de hachage. Vous avez eu l'idée. Pour un identifiant composé, renvoyez le champ (ou hashCode de celui-ci) avec les valeurs les plus distinctes. Moins est plus.

Bien sûr, le contrat entre hashCode () et equals () doit être respecté. Ainsi, equals () doit être implémenté en conséquence. hashCode () n'a pas besoin d'utiliser les qualificatifs complets nécessaires pour l'égalité, mais tous les champs utilisés dans hashCode () doivent être utilisés dans equals (). Ici, des méthodes comme StringUtils.equals (s1, s2) sont utiles pour gérer les valeurs nulles de manière cohérente et en toute sécurité.

2
Charlie