Bonjour ! Dans la leçon précédente, nous avons appris à choisir la collection (List, Set, Map) la mieux adaptée à un problème donné. Nous avons vu que les Set et les clés des Map reposent sur un concept fondamental : l'unicité. Jusqu'à présent, nous avons surtout utilisé des types simples comme String ou Integer, pour lesquels Java sait déjà ce que "unique" signifie.
Mais que se passe-t-il lorsque nous voulons utiliser nos propres objets, comme une classe Produit ou Utilisateur, dans un HashSet ou comme clé dans un HashMap ? Comment Java peut-il savoir si deux objets Produit sont "égaux" ? C'est le cœur de la leçon d'aujourd'hui.
Nous allons explorer deux méthodes fondamentales héritées de la classe Object : equals() et hashCode(). Comprendre et savoir redéfinir correctement ces méthodes est une compétence cruciale qui garantit le bon fonctionnement de vos objets personnalisés avec le framework de collections. C'est le pont parfait entre notre module sur les collections et le prochain sur la programmation orientée objet.
1. Le problème : le comportement par défaut
Imaginons que nous développions un système pour gérer des contacts. Nous créons une classe simple Contact :
public class Contact {
private String nom;
private String email;
public Contact(String nom, String email) {
this.nom = nom;
this.email = email;
}
@Override
public String toString() {
return "Contact{" + "nom='" + nom + '\'' + ", email='" + email + '\'' + '}';
}
}
Maintenant, essayons d'ajouter deux contacts qui représentent la même personne dans un HashSet, qui, nous le savons, ne doit pas contenir de doublons.
import java.util.HashSet;
import java.util.Set;
public class TestContacts {
public static void main(String[] args) {
Set<Contact> contacts = new HashSet<>();
Contact c1 = new Contact("Alice", "alice@email.com");
Contact c2 = new Contact("Alice", "alice@email.com");
contacts.add(c1);
contacts.add(c2);
System.out.println("Taille du Set : " + contacts.size()); // Affiche 2
System.out.println("Contenu du Set : " + contacts); // Affiche les deux objets
}
}
Résultat inattendu : la taille du Set est 2 ! Pourtant, les données des deux objets sont identiques. Pourquoi HashSet les considère-t-il comme différents ?
La raison est que, par défaut, la méthode equals() héritée de la classe Object compare les références en mémoire des objets, pas leurs contenus. C'est l'équivalent de l'opérateur ==. Comme c1 et c2 sont deux objets distincts créés avec new, ils ont des adresses mémoire différentes. Pour Java, ils ne sont donc pas "égaux".
Pour résoudre ce problème, nous devons dire à Java ce que signifie "être égal" pour nos objets Contact. C'est là qu'intervient la redéfinition de equals().
2. Redéfinir equals() pour une égalité métier
Nous allons redéfinir (override) la méthode equals() dans notre classe Contact pour qu'elle compare les champs qui définissent l'identité d'un contact, par exemple, le nom et l'email.
L'article suivant explique très bien comment implémenter equals() en respectant les bonnes pratiques.
méthodes equals et hashCode : bonnes pratiques
Cet article de CodeGym détaille les règles et les pièges à éviter lors de la redéfinition de equals() et hashCode().
Lisez la section intitulée "méthode equals()". Concentrez-vous sur les vérifications essentielles à effectuer avant de comparer les champs : La comparaison avec soi-même (this == o). La vérification de null. La comparaison des types de classes (getClass()). Notez également le contrat de la méthode equals() (réflexivité, symétrie, etc.).
En nous basant sur cette lecture, implémentons equals() dans notre classe Contact. Deux contacts seront considérés comme égaux si leur champ email est identique.
import java.util.Objects;
public class Contact {
// ... attributs et constructeur ...
@Override
public boolean equals(Object o) {
// 1. Vérifier si les références sont identiques (optimisation)
if (this == o) return true;
// 2. Vérifier si l'objet passé est null ou d'un type différent
if (o == null || getClass() != o.getClass()) return false;
// 3. "Caster" l'objet pour pouvoir accéder à ses champs spécifiques
Contact contact = (Contact) o;
// 4. Comparer les champs pertinents pour l'égalité métier
// On considère que l'email est l'identifiant unique.
return Objects.equals(email, contact.email);
}
// ... toString() ...
}
Note : L'utilisation de Objects.equals() est une bonne pratique. Elle gère correctement le cas où les champs comparés seraient null.
Si nous relançons notre programme de test maintenant... le HashSet contient toujours 2 éléments ! Qu'avons-nous manqué ?
3. Le contrat equals() / hashCode()
Les collections basées sur le hachage, comme HashSet et HashMap, utilisent une stratégie en deux étapes pour être ultra-performantes :
- Hachage : D'abord, elles appellent la méthode
hashCode()de l'objet. Cette méthode retourne un entier (int) qui agit comme un "index" rapide. L'objet est placé dans un "seau" (bucket) correspondant à cette valeur de hachage. - Égalité : Si plusieurs objets se retrouvent dans le même seau (ce qu'on appelle une "collision"), et seulement à ce moment-là, la collection utilise la méthode
equals()pour vérifier s'il s'agit réellement de doublons.
L'image ci-dessous illustre ce mécanisme :

Le problème dans notre code est que nous avons redéfini equals() mais pas hashCode(). Les deux objets c1 et c2, bien qu'égaux selon notre nouvelle méthode equals(), retournent probablement des hashCode différents (car la méthode par défaut de Object est souvent basée sur l'adresse mémoire). Le HashSet les place donc dans des seaux différents et ne pense même pas à appeler equals() !
Cela nous amène à la règle la plus importante de cette leçon.

La règle d'or : Si deux objets sont égaux selon equals(), alors ils doivent avoir la même valeur de hashCode().
La vidéo suivante résume très bien cette relation cruciale.
05 - Méthode hashCode(), liée à la méthode equals()
Cette vidéo de la chaîne cours-en-ligne explique le lien indispensable entre equals() et hashCode().
Regardez ces deux courts passages : Le couplage : l'auteur explique que equals() est couplée à hashCode(). La règle fondamentale : concentrez-vous sur la règle "deux objets égaux au sens de equals doivent avoir le même hashCode".
4. Redéfinir hashCode()
Pour respecter le contrat, nous devons implémenter hashCode() en utilisant les mêmes champs que ceux que nous avons utilisés dans equals(). Dans notre cas, il s'agit du champ email.
La manière la plus simple et la plus sûre de le faire aujourd'hui est d'utiliser la méthode utilitaire Objects.hash().
Ajoutons hashCode() à notre classe Contact :
import java.util.Objects;
public class Contact {
// ... attributs, constructeur, equals() ...
@Override
public int hashCode() {
// Utiliser les mêmes champs que dans equals()
return Objects.hash(email);
}
// ... toString() ...
}
Désormais, notre classe Contact respecte le contrat. Relançons notre test.
// ...
contacts.add(c1);
contacts.add(c2);
System.out.println("Taille du Set : " + contacts.size()); // Affiche 1
System.out.println("Contenu du Set : " + contacts); // Affiche un seul contact
Victoire ! Le HashSet fonctionne maintenant comme prévu. Il reconnaît que c1 et c2 sont des doublons et n'en conserve qu'un seul.
L'article suivant, très pratique, vous montre un exemple complet avec une "mauvaise" et une "bonne" implémentation, ce qui renforce tout ce que nous venons de voir.
Java - HashSet et TreeSet : Guide complet des ensembles
Ce tutoriel de Rachid Saâd Academy illustre par l'exemple le problème et la solution.
Lisez la section "Gestion des objets personnalisés (hashCode() et equals())". Comparez le code de PersonneMauvais (sans surcharge) et PersonneBon (avec surcharge). Observez comment la sortie du programme change radicalement, et lisez les "Règles d'or" qui résument parfaitement les bonnes pratiques.
Bonne pratique professionnelle : La plupart des IDE modernes (comme IntelliJ IDEA ou VS Code) peuvent générer automatiquement des implémentations correctes de equals() et hashCode(). C'est une fonctionnalité très utile qui vous évite d'écrire ce code répétitif et prévient les erreurs. Cherchez une option comme "Generate..." ou "Source Action..." dans votre IDE.
Conclusion
Vous venez de maîtriser l'un des aspects les plus importants (et souvent source de bugs pour les débutants) de la programmation objet en Java. Savoir comment les objets interagissent au sein des collections est essentiel pour construire des applications robustes.
Points clés à retenir :
- Par défaut,
equals()compare les adresses mémoire (==). - Pour utiliser vos propres objets dans des collections basées sur le hachage (
HashSet,HashMap), vous devez redéfinirequals()ethashCode(). - Le contrat : Si
a.equals(b)est vrai, alorsa.hashCode()doit être égal àb.hashCode(). - Les deux méthodes doivent se baser sur le même ensemble de champs qui définissent l'identité métier de votre objet.
- Utilisez
Objects.equals()etObjects.hash()pour des implémentations sûres et concises, ou laissez votre IDE les générer pour vous.
Cette leçon conclut notre module sur les collections Java. Vous êtes maintenant armé pour non seulement choisir la bonne structure de données, mais aussi pour vous assurer que vos propres types d'objets s'y comportent correctement.
Dans le prochain module, nous allons formaliser et approfondir tout ce qui concerne la création de nos propres types. Nous entrerons de plain-pied dans la Programmation Orientée Objet (POO) en commençant par les bases : comment définir des classes, des attributs, des méthodes et comment créer des instances de ces classes.