Le style D'utilisation de HashCodeBuilder et EqualsBuilder
j'utilise souvent apache HashCodeBuilder et EqualsBuilder pour l'égalité des objets en utilisant la réflexion, mais récemment un collègue m'a dit que l'utilisation de la réflexion peut causer un énorme succès de performance si l'entité contient beaucoup de propriétés. Craignant que je puisse utiliser une mauvaise implémentation, ma question Est, laquelle des approches suivantes préféreriez-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));
}
}
6 réponses
bien sûr, la deuxième option est plus élégante et simple. Mais si vous êtes préoccupé par la performance, vous devriez choisir la première approche, la deuxième méthode échoue aussi si un gestionnaire de sécurité est en cours d'exécution.
j'opterai pour la première option si j'étais dans votre situation. Il y a aussi une erreur dans votre première approche en générant hashCode ça devrait être le constructeur du retour.toHashCode(); au lieu de rendement du générateur.hashCode();(qui renvoie hashcode générateur d'objets hashcode)
même si la deuxième option est plus attrayante (parce qu'elle n'est qu'une ligne de code), je choisirais la première option.
La raison en est simplement la performance. Après avoir fait un petit test, j'ai trouvé une grande différence de temps entre eux.
pour 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 des égaux différents 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);
}
}
}
Le test sont assez simple et ne sert qu'à mesurer le temps.
les résultats ont été Les suivants:
- test1 (2 024 s)
- test2 (0,039 s)
je les ai choisis pour être complètement égal, afin d'avoir le plus de temps. Si vous choisissez de faire le test avec les conditions NotEquals vous aurez plus court temps, mais en gardant un très grand décalage horaire aussi.
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 performance afin d'enregistrer quelques lignes de code que vous ne pouvez pas taper de toute façon en utilisant un modèle (dans Eclipse sous Windows - > les préférences se trouvent dans Java - > Editor - > Templates) tel que ceci:
${: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();
}
je préfère la deuxième option pour 2 raisons:
évidemment il est plus facile de lire
Je n'achèterais pas d'arguments de performance pour la première option, à moins qu'ils n'incluent une métrique pertinente. Par exemple: combien de millisecondes les "égaux" basés sur la réflexion ajouteraient-ils à une latence de requête de bout en bout typique? Dans l'ensemble, quelle serait l'augmentation en pourcentage? Sans savoir que les bonnes chances sont que l'optimisation est prématurée
Votre question écrite illustre clairement l'un des avantages de la seconde approche:
Dans le premier cas, c'est très facile de faire l'erreur return builder.hashCode()
, au lieu de les corriger return builder.toHashCode()
, qui résultera en des erreurs subtiles qui peuvent être très difficiles à dépister.
le second cas élimine la possibilité de cette typographie, résultant en moins de frapper votre tête dans le clavier en essayant de trouver le bug.
je dirais que ni l'un ni l'autre n'est une bonne mise en oeuvre. Je dirais que EqualsBuilder n'est pas un bon cadre à utiliser pour les raisons suivantes:
- non extensible. Que faire si l'un des champs que vous essayez d'affirmer l'égalité doit traiter les valeurs null et vierge comme égaux?
- vous devez maintenir la liste des variables comme si elles étaient codées en dur. Ce qui signifie que vous devez énumérer toutes les variables que vous voulez comparer. À ce stade, il n'y a pas différents entre un == o.getA() && b == o.getB() ...
- utiliser 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 dirais qu'il doit y avoir un meilleur cadre que celui D'Apache.
d'Accord avec @Churk, Apache HashCodeBuilder et EqualsBuilder ne sont pas bien mises en œuvre. HashCodeBuilder joue toujours avec les nombres premiers! En outre, il fait beaucoup de travail inutile. Avez-vous lu la source?
depuis Java 5 (si pas plus tôt), AbstractHashMap<> n'a pas utilisé modulo d'un nombre premier pour localiser le seau de hachage. Au lieu de cela, le nombre de seaux est une puissance de deux et le N d'ordre inférieur bits du code de hachage est utilisé pour localiser le seau.
de plus, il "mélange" le code de hachage fourni par la demande de façon à ce que les bits soient répartis uniformément et, par conséquent, que les seaux soient remplis uniformément.
ainsi, la bonne façon d'outrepasser l'objet int.hashCode () est en retournant la valeur constante la plus simple avec la plus haute arité dans la population d'objets qui cohabiteront en toute collection utiliser la classe.
Typiquement, la valeur ID non modifié est votre meilleur pari. Si votre champ ID est intégral, jetez-le à (int) et retournez-le. Si C'est une chaîne ou un autre objet, il suffit de retourner son code de hachage. Vous obtenez l'idée. Pour un identificateur de composé, retournez le champ (ou son hashCode) avec les valeurs les plus distinctes. Moins est plus.
bien sûr, le contrat entre hashCode() et equals() doit être respecté. Ainsi, equal () devrait être mis en œuvre en conséquence. hashCode() n'a pas besoin d'utiliser pleinement les qualificatifs nécessaires pour l'égalité, mais tous les champs utilisés dans hashCode() doit être utilisé dans equals(). Ici, les méthodes comme les StringUtils.les equals (s1, s2 ) sont utiles pour gérer les valeurs nulles de manière cohérente et sûre.