Je suis curieux de savoir ce que les gens ici pensent de l'utilisation de org.Apache.commons.lang.builder
EqualsBuilder
/HashCodeBuilder
pour implémenter le equals
/hashCode
? Serait-ce une meilleure pratique que d'écrire la vôtre? Est-ce que ça fonctionne bien avec Hibernate? Qu'en penses-tu?
Les constructeurs de commons/lang sont excellents et je les utilise depuis des années sans perte de performances notable (avec et sans veille prolongée). Mais comme l'écrit Alain, la méthode Guava est encore plus agréable:
Voici un exemple de haricot:
public class Bean{
private String name;
private int length;
private List<Bean> children;
}
Voici equals () et hashCode () mis en œuvre 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 l'ont souligné les commentaires, cette fonctionnalité a depuis été introduite dans le JDK. Par conséquent, Guava n'est plus nécessaire.
Comme vous pouvez le constater, la version de Guava/JDK est plus courte et évite les objets auxiliaires superflus. Dans le cas d’égal à égal, cela permet même de court-circuiter l’évaluation si un précédent appel Object.equals()
renvoie false (pour être juste: commons/lang utilise une méthode ObjectUtils.equals(obj1, obj2)
avec une sémantique identique qui pourrait être utilisé à la place de EqualsBuilder
pour permettre le court-circuitage comme ci-dessus).
Donc: oui, les constructeurs communs de langage sont très préférables aux méthodes equals()
et hashCode()
construites manuellement (ou ces monstres terribles qu'Eclipse générera pour vous), mais le Java 7+/Guava sont encore meilleures.
Et une note sur Hibernate:
soyez prudent lorsque vous utilisez des collections paresseuses dans vos implémentations equals (), hashCode () et toString (). Cela échouera misérablement si vous n'avez pas de session ouverte.
Note (à propos d'égal à é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 modifier la ou les lignes.
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 voudrez probablement aussi appeler super(equals())
dans votre méthode equals()
. Les avis diffèrent ici, le sujet est traité dans cette question:
bonne façon d'intégrer la super-classe dans une implémentation de Guava Objects.hashcode ()?
(bien qu'il s'agisse de hashCode()
, il en va de même pour equals()
)
Note (inspiré par le commentaire de kayahr )
Objects.hashCode(..)
(tout comme le Arrays.hashCode(...)
sous-jacent) pourrait mal fonctionner si vous avez plusieurs champs primitifs. Dans de tels cas, EqualsBuilder
peut en fait être la meilleure solution.
Si vous ne voulez pas dépendre d'une bibliothèque tierce (peut-être que vous utilisez un périphérique avec des ressources limitées) et que vous ne voulez même pas taper vos propres méthodes, vous pouvez également laisser le IDE = faire le travail, par exemple en utilisation Eclipse
Source -> Generate hashCode() and equals()...
Vous obtiendrez du code "natif" que vous pouvez configurer à votre guise et que vous devez obligatoirement supporter les modifications.
Exemple (Eclipse 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;
}
}
EqualsBuilder et HashCodeBuilder ont deux aspects principaux qui diffèrent du code écrit manuellement:
EqualsBuilder et HashCodeBuilder facilitent la comparaison des champs pouvant être nuls. Avec un code écrit manuellement, cela crée beaucoup de passe-partout.
EqualsBuilder créera par contre une instance par appel de méthode égal. Si vos méthodes equals sont appelées souvent, cela créera beaucoup d'instances.
Pour Hibernate, les implémentations equals et hashCode ne font aucune différence. Ils sont juste un détail de mise en œuvre. Pour presque tous les objets de domaine chargés avec la mise en veille prolongée, la surcharge d'exécution (même sans analyse d'échappement) du générateur peut être ignorée. La surcharge de la base de données et de la communication sera importante.
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 lente et "l'implémentation" ne sera pas correcte pour toutes les classes sauf les plus simples. Prendre en compte tous les membres est également dangereux car les membres nouvellement introduits changent le comportement de la méthode equals. La version de réflexion peut être utile dans le code de test.
Si vous n’écrivez pas le vôtre, vous avez également la possibilité d’utiliser google guava (anciennement google collections)
À 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é commerciale à utiliser dans equals () et hashCode (), et ils ont leurs raisons. Si vous utilisez les générateurs auto equals () et hashCode () sur votre clé d'entreprise, 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 mauvais à l’OMI. Par exemple, je travaille actuellement sur un projet dans lequel des entités sont écrites en utilisant Pojomatic avec @AutoProperty, ce que je considère comme un très mauvais modèle.
Leurs deux scénarios principaux pour utiliser hashCode () et equals () sont:
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 id et leur classe/table sont égaux). Mais quand on implémente auto equals () un hashCode () sur tous les accessoires, qu'avons-nous?
Ainsi, pour 99% des projets que je réalise, nous utilisons les implémentations suivantes de equals () et hashCode () écrites une fois dans la classe d'entité de base, ce qui est cohérent avec les concepts d'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 lors de l’étape de persistance, c’est-à-dire. J'utilise l'instance match. Pour les objets persistants, je compare la clé unique, qui est la table/id (je n’utilise jamais de clés composites).
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’objet mentionnée ci-dessus (en fait, la surcharge de la méthode Objects.hash () est encore plus grande lorsque vous avez héritage car cela 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, pensez à refactoriser et à introduire plus de classe au lieu de conserver un tas de chaînes et de primitives.
Les inconvénients sont les suivants: ce n'est pas utile si vous avez principalement des primitives et/ou des tableaux que vous devez hacher en profondeur. (C'est normalement le cas lorsque vous devez gérer des objets plats (de transfert) hors de votre contrôle).
Si vous ne faites que traiter avec le bean entity 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();
}