CodeGym /Cours /JAVA 25 SELF /Contrats equals et hashCode

Contrats equals et hashCode

JAVA 25 SELF
Niveau 29 , Leçon 0
Disponible

1. Introduction

Comment nous comparons les objets

En Java, les objets ne sont pas que des données : chacun a sa propre adresse en mémoire. L’opérateur == répond à la question « est-ce la même boîte ? », c’est-à-dire qu’il compare les références (adresses) et non le contenu. La méthode equals est conçue pour comparer le contenu. Par défaut, si elle n’est pas redéfinie, equals se comporte comme ==.

Person p1 = new Person("Ivan", 20);
Person p2 = new Person("Ivan", 20);

System.out.println(p1 == p2); // false — ce sont des objets différents en mémoire !

Pour que deux objets distincts soient considérés égaux par leurs données (par exemple, si tous les champs significatifs coïncident), il faut redéfinir equals. Et si vous prévoyez d’utiliser les objets dans des collections de hachage, il est indispensable de redéfinir correctement hashCode en même temps.

class Person {
    String name;
    int age;

    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true; // c'est le même objet
        if (o == null || getClass() != o.getClass()) return false; // vérifier la classe
        Person person = (Person) o; // conversion vers le type requis
        return age == person.age && name.equals(person.name); // comparer les champs
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age); // pour que cela fonctionne avec HashSet/HashMap
    }
}

Une telle redéfinition est cruciale pour fonctionner correctement avec HashSet/HashMap : sans equals et hashCode, les collections considéreront différents même des objets aux données identiques.

La relation d’équivalence dans equals dépend de vos exigences : on peut comparer sur tous les champs, sur une partie des champs (par exemple uniquement l’email d’un User) — l’important est la cohérence et le respect du contrat.

Où est-ce particulièrement important ?

  • Dans les collections basées sur des tables de hachage : HashSet, HashMap, LinkedHashSet, etc.
  • Lors de la recherche et de la suppression d’éléments dans les collections : sans equals correct, l’objet voulu peut « ne pas être trouvé ».
  • Dans la logique métier : par exemple, deux User ayant le même email doivent être considérés comme un seul utilisateur.

hashCode — à quoi sert-il ?

Les collections de hachage (par exemple, HashSet, HashMap) utilisent des tables de hachage. La méthode hashCode calcule un entier — « l’index du bucket », où l’objet ira. Si deux objets sont égaux selon equals, leurs hashCode doivent coïncider. Si vous violez cette règle, les collections se mettront à se comporter de façon imprévisible.

2. Contrat equals et hashCode

Contrat equals

Exigences principales pour le comportement de equals :

  • Réflexivité : a.equals(a) est toujours true.
  • Symétrie : si a.equals(b) est true, alors b.equals(a) est true aussi.
  • Transitivité : si a.equals(b) et b.equals(c), alors a.equals(c) est true également.
  • Cohérence : tant que les objets ne changent pas, le résultat des appels reste stable.
  • Comparaison avec null : tout objet est différent de null.

Contrat hashCode

  • Si deux objets sont égaux selon equals, leurs hashCode sont égaux.
  • Si des objets ne sont pas égaux, leurs codes de hachage peuvent coïncider (les collisions sont possibles mais indésirables).
  • Tant que l’objet ne change pas logiquement, son hashCode doit rester constant.

En d’autres termes, un hashCode identique est une condition nécessaire mais non suffisante à l’égalité : un même hachage ne garantit pas l’égalité selon equals.

3. Implémentation de equals et hashCode : exemple

Considérons la classe Person, où l’égalité est définie par les champs name et age.

public class Person {
    private String name;
    private int age;

    // Constructeur, getters, setters...

    @Override
    public boolean equals(Object o) {
        if (this == o) return true; // Comparaison des références
        if (o == null || getClass() != o.getClass()) return false; // Vérification de la classe

        Person person = (Person) o; // Conversion de type

        // Comparer les champs
        return age == person.age &&
               (name != null ? name.equals(person.name) : person.name == null);
    }

    @Override
    public int hashCode() {
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + age; // 31 — un choix courant de nombre premier
        return result;
    }
}
  • D’abord, des vérifications rapides : référence et classe.
  • Ensuite, comparaison des champs significatifs.
  • Dans hashCode, on utilise le nombre premier 31 pour réduire les collisions.

Utiliser Objects.equals et Objects.hash

Depuis Java 7, la classe Objects simplifie le code et le rend plus sûr vis-à-vis de null :

import java.util.Objects;

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Person person = (Person) o;
    return age == person.age &&
           Objects.equals(name, person.name);
}

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

4. equals, hashCode et compareTo : quel lien

Quel lien entre equals et compareTo ?

L’interface Comparable définit la méthode compareTo, qui renvoie un nombre négatif/zéro/positif pour « moins/égal/plus ». Il est souhaitable que de a.compareTo(b) == 0 découle a.equals(b). L’inverse n’est pas obligatoire.

Si vous violez la cohérence (par exemple, compareTo compare uniquement par l’âge, alors que equals compare par le nom et l’âge), des collections triées comme TreeSet/TreeMap peuvent se comporter de manière inattendue : les objets sont considérés « égaux » du point de vue de l’ordre, mais pas égaux par le contenu.

equals et hashCode dans les collections

  • Dans HashSet et HashMap, les opérations d’ajout/de recherche/de suppression s’appuient sur une implémentation correcte de equals et hashCode.
  • Sans redéfinition, ces collections considèrent les objets « différents », même si leurs données sont identiques.

5. Exemples : comment cela fonctionne dans les collections

HashSet : stockage d’objets uniques

Set<Person> people = new HashSet<>();
people.add(new Person("Ivan", 20));
people.add(new Person("Ivan", 20)); // Doublon

System.out.println(people.size()); // 1 si equals/hashCode sont correctement implémentés

Sans equals/hashCode corrects, les deux objets entreront dans l’ensemble.

HashMap : recherche par clé

Map<Person, String> map = new HashMap<>();
Person p1 = new Person("Anna", 25);
Person p2 = new Person("Anna", 25);

map.put(p1, "Utilisateur 1");
System.out.println(map.get(p2)); // "Utilisateur 1", si equals/hashCode sont correctement implémentés

Sans contrat, la collection renverra null — pour elle, ce sont des clés « différentes ».

6. Bonnes pratiques : conseils d’implémentation

  • Incluez dans equals/hashCode tous les champs qui définissent « l’identité » de l’objet.
  • N’utilisez pas de champs mutables (qui changent après l’ajout dans des collections) pour calculer le hashCode.
  • Laissez l’IDE générer les méthodes — moins de risques de fautes de frappe.
  • Dans equals, vérifiez d’abord this == o, puis la classe, puis les champs.
  • Pour comparer des champs-objets, utilisez Objects.equals.
  • Pour le code de hachage, utilisez Objects.hash ou un modèle éprouvé avec le multiplicateur 31.

7. Nuances utiles

Pourquoi ne pas utiliser uniquement hashCode ?

Les collisions sont inévitables : des objets différents peuvent avoir le même hashCode. Le hachage n’est qu’un repère rapide vers un bucket ; la décision finale d’égalité appartient à equals.

Peut-on ne pas redéfinir equals et hashCode ?

Seulement si vous êtes certain que les objets ne seront jamais comparés par leur contenu et ne deviendront ni clés ni éléments uniques dans des collections. En pratique, c’est rare.

Différence entre ==, equals et compareTo

Opérateur/méthode Que compare-t-il ? À quoi sert-il ?
==
Références (adresses en mémoire) Vérifier « même objet ? »
equals
Contenu des objets Égalité selon la logique métier
compareTo
Ordre (moins/égal/plus) Tri, ordonnancement

8. Erreurs courantes lors de l’implémentation de equals et hashCode

Erreur n°1 : vous avez redéfini equals mais oublié hashCode. Les objets sont considérés égaux, mais aboutissent dans des buckets différents de la table de hachage — la recherche et la suppression se cassent.

Erreur n°2 : vous utilisez des champs mutables dans hashCode. Si un champ change après l’ajout dans la collection, l’objet « se perd » : le hachage change mais pas le bucket.

Erreur n°3 : symétrie/transitivité de equals violée. a.equals(b) renvoie true, alors que b.equals(a) renvoie false, ou la transitivité est rompue — les collections se mettent à se comporter de manière imprévisible.

Erreur n°4 : vous ne vérifiez pas la classe dans equals. Comparer des objets de classes différentes conduit à des résultats incorrects ou à des exceptions.

Erreur n°5 : vous comparez des chaînes et des objets avec ==. L’opérateur == compare les références ; utilisez equals pour le contenu.

Erreur n°6 : absence de cohérence entre compareTo et equals. Si a.compareTo(b) == 0, mais que !a.equals(b), les collections TreeSet/TreeMap peuvent considérer les éléments égaux pour l’ordre mais différents pour l’égalité — source de « fantômes » et de doublons.

Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION