Aller au contenu principal

Égalité des Objets - equals() et hashCode()

En Java, déterminer si deux objets sont "égaux" est un concept fondamental, surtout lorsque l'on travaille avec des collections. Java fournit deux méthodes dans la classe Object qui sont cruciales pour définir et gérer l'égalité des objets : equals() et hashCode(). Comprendre leur contrat et comment les surcharger correctement est vital pour des applications robustes.

1. La méthode equals()

La méthode equals() est utilisée pour déterminer si deux objets sont logiquement égaux, en se basant sur leur contenu plutôt que sur leur adresse mémoire.

Implémentation par défaut (Object.equals(Object obj)): L'implémentation par défaut de equals() dans la classe Object compare simplement les adresses mémoire des deux objets, ce qui signifie que obj1.equals(obj2) est équivalent à obj1 == obj2. Elle retourne true uniquement si les deux références d'objets pointent vers le même objet en mémoire.

public boolean equals(Object obj) {
return (this == obj); // Vérifie l'égalité de référence
}

Pourquoi surcharger equals()? Pour la plupart des classes personnalisées, vous voudrez définir l'égalité basée sur l'état (les valeurs de leurs champs) des objets, et non seulement sur le fait qu'ils sont la même instance. Par exemple, deux objets Person pourraient être considérés comme égaux s'ils ont le même id et le même name, même s'il s'agit d'objets distincts en mémoire.

Contrat de equals() (tel que spécifié dans java.lang.Object): Lorsque vous surchargez equals(), vous devez adhérer à ces cinq propriétés pour qu'elle se comporte correctement et de manière prévisible, en particulier lorsqu'elle est utilisée dans des collections :

  1. Réflexivité: Pour toute valeur de référence non nulle x, x.equals(x) doit retourner true. (Un objet doit être égal à lui-même).
  2. Symétrie: Pour toutes valeurs de référence non nulles x et y, x.equals(y) doit retourner true si et seulement si y.equals(x) retourne true. (Si A est égal à B, alors B doit être égal à A).
  3. Transitivité: Pour toutes valeurs de référence non nulles x, y et z, si x.equals(y) retourne true et y.equals(z) retourne true, alors x.equals(z) doit retourner true. (Si A est égal à B, et B est égal à C, alors A doit être égal à C).
  4. Consistance: Pour toutes valeurs de référence non nulles x et y, de multiples invocations de x.equals(y) doivent systématiquement retourner true ou systématiquement retourner false, à condition qu'aucune information utilisée dans les comparaisons equals sur les objets ne soit modifiée. (L'égalité ne change pas à moins que l'état de l'objet ne change).
  5. Gestion du null: Pour toute valeur de référence non nulle x, x.equals(null) doit retourner false. (Un objet n'est jamais égal à null).

Modèle typique d'implémentation de equals():

public class MyClass {
private int id;
private String name;

public MyClass(int id, String name) {
this.id = id;
this.name = name;
}

@Override
public boolean equals(Object o) {
// 1. Vérifier l'égalité de référence (optimisation)
if (this == o) return true;
// 2. Vérifier null et le type de classe
if (o == null || getClass() != o.getClass()) return false;
// Ou si vous utilisez instanceof pour le polymorphisme (attention au LSP et à la symétrie) :
// if (!(o instanceof MyClass)) return false;

// 3. Caster l'objet au type de la classe courante
MyClass myClass = (MyClass) o;

// 4. Comparer les champs pertinents
if (id != myClass.id) return false;
return name != null ? name.equals(myClass.name) : myClass.name == null;
}

// ... getters, setters, etc.
}

Pièges courants avec equals():

  • Violation de la symétrie (par exemple, avec instanceof et l'héritage): Si A.equals(B) est vrai, B.equals(A) doit être vrai. L'utilisation de instanceof pour la vérification de type peut parfois violer la symétrie si des sous-classes sont impliquées (par exemple, ColorPoint étend Point et equals vérifie instanceof Point). Il est généralement plus sûr d'utiliser getClass() != o.getClass().
  • Oubli de surcharger hashCode(): C'est le piège le plus critique, conduisant à un comportement incorrect dans les collections basées sur le hachage.

2. La méthode hashCode()

La méthode hashCode() retourne une valeur de code de hachage entière pour l'objet. Ce code de hachage est principalement utilisé par les collections basées sur le hachage (comme HashMap, HashSet, Hashtable) pour déterminer où stocker et retrouver les objets efficacement.

Implémentation par défaut (Object.hashCode()): L'implémentation par défaut de hashCode() dans Object retourne généralement un entier distinct pour des objets distincts. Il s'agit souvent de l'adresse mémoire de l'objet convertie en entier, ou d'un autre identifiant unique basé sur la représentation interne de l'objet.

Pourquoi surcharger hashCode()? Si vous surchargez equals(), vous devez également surcharger hashCode() pour maintenir le contrat général de Object.hashCode().

Contrat de hashCode() (tel que spécifié dans java.lang.Object):

  1. Consistance: Chaque fois qu'elle est invoquée sur le même objet plus d'une fois pendant l'exécution d'une application Java, la méthode hashCode doit retourner le même entier de manière cohérente, à condition qu'aucune information utilisée dans les comparaisons equals sur l'objet ne soit modifiée. Cet entier n'a pas besoin de rester cohérent d'une exécution de l'application à l'autre.
  2. L'égalité implique l'égalité des codes de hachage: Si deux objets sont égaux selon la méthode equals(Object), alors l'appel de la méthode hashCode sur chacun des deux objets doit produire le même résultat entier.
  3. L'inégalité n'implique PAS l'inégalité des codes de hachage: Il n'est pas requis que si deux objets sont inégaux selon la méthode equals(Object), alors l'appel de la méthode hashCode sur chacun des deux objets doive produire des résultats entiers distincts. Cependant, la production de résultats entiers distincts pour des objets inégaux peut améliorer la performance des tables de hachage. (Les collisions sont autorisées mais doivent être minimisées).

Pourquoi le contrat equals() et hashCode() est crucial :

Les collections basées sur le hachage fonctionnent en calculant d'abord le hashCode() d'un objet pour déterminer à quel "compartiment" il appartient. Ce n'est qu'ensuite qu'elles utilisent equals() pour comparer les objets au sein de ce compartiment.

  • Si vous surchargez equals() mais pas hashCode() :
    • obj1.equals(obj2) pourrait retourner true (car leur contenu est le même).
    • Mais obj1.hashCode() et obj2.hashCode() pourraient retourner des valeurs différentes (en raison de l'implémentation par défaut qui retourne des valeurs distinctes pour des objets distincts).
    • Cela signifie que HashMap ou HashSet pourrait placer obj1 et obj2 dans des compartiments différents, et vous ne seriez pas en mesure de récupérer obj1 en utilisant obj2 comme clé, ou un HashSet pourrait contenir deux objets "égaux". Cela rompt le comportement attendu de ces collections.

Modèle typique d'implémentation de hashCode(): Une bonne implémentation de hashCode() combine les codes de hachage de tous les champs utilisés dans equals(). Pour les champs primitifs, utilisez leur code de hachage direct (ou celui de leur classe enveloppe hashCode()). Pour les types de référence, utilisez leur hashCode(). Les valeurs null nécessitent un traitement spécial (par exemple, retourner 0 ou une constante fixe).

import java.util.Objects; // Utilitaire pour générer facilement des codes de hachage

public class MyClass {
private int id;
private String name;

// Constructeur, equals() comme ci-dessus...

@Override
public int hashCode() {
// Utiliser Objects.hash() est la méthode la plus simple et recommandée
return Objects.hash(id, name);

// Exemple d'implémentation manuelle :
// int result = id;
// result = 31 * result + (name != null ? name.hashCode() : 0);
// return result;
}
}

Le nombre 31 est un multiplicateur premier courant utilisé dans les calculs de hashCode() car c'est un nombre premier impair. Multiplier par un nombre premier impair minimise les collisions et fonctionne bien dans les scénarios typiques.

Bonnes pratiques

  1. Toujours surcharger les deux: Si vous surchargez equals(), vous devez surcharger hashCode().
  2. Utiliser les champs utilisés dans equals(): Assurez-vous que les champs utilisés pour calculer hashCode() sont les mêmes champs exacts utilisés dans la comparaison equals().
  3. Consistance: Le hashCode() doit retourner la même valeur pour un objet tant que les champs utilisés dans equals() ne changent pas. Si les champs d'un objet changent, son code de hachage pourrait changer, ce qui peut causer des problèmes si l'objet est déjà stocké dans une collection basée sur le hachage (il pourrait devenir "perdu"). Pour cette raison, les objets utilisés comme clés dans HashMap ou éléments dans HashSet sont souvent rendus immuables ou au moins les champs utilisés dans equals() et hashCode() sont immuables.
  4. Génération par l'IDE: Les IDE modernes (comme IntelliJ IDEA, Eclipse) peuvent générer automatiquement les méthodes equals() et hashCode() en fonction des champs que vous sélectionnez. C'est fortement recommandé car cela aide à garantir la justesse et à adhérer aux bonnes pratiques.
  5. Objects.hash(): Depuis Java 7, java.util.Objects.hash(Object... values) fournit un moyen pratique de générer des codes de hachage à partir de plusieurs champs, en gérant correctement les valeurs nulles.
  6. Considérations de performance: Bien que l'objectif principal soit la justesse, un code de hachage bien distribué (minimisant les collisions) est important pour la performance des collections basées sur le hachage.

En implémentant correctement equals() et hashCode(), vous vous assurez que vos objets personnalisés se comportent comme prévu dans tous les contextes où l'égalité des objets est importante, en particulier lorsqu'ils sont utilisés dans le puissant framework de collections de Java.