Java equals() et hashCode()
Redéfinissez correctement equals() et hashCode() dans vos classes Java pour prendre en charge les collections et l'égalité basée sur les valeurs.
equals et hashCode sont les deux méthodes de Object sur lesquelles les collections basées sur le hachage — HashMap, HashSet, LinkedHashMap, tout ce qui repose sur le hachage — s'appuient silencieusement. Implémentez-les correctement et vos objets se comportent comme des valeurs : set.contains(point) trouve le point quelle que soit l'instance new Point(3, 4) que vous passez. Implémentez-les incorrectement et vous obtenez des doublons dans les ensembles, des clés manquantes dans les maps, et des bugs qui n'apparaissent que sous charge.
La version héritée de Object compare l'identité : deux références sont égales uniquement lorsqu'elles pointent vers le même objet. C'est adapté pour des choses comme les connexions de base de données, où chaque instance est une ressource distincte. Pour les classes de type valeur — argent, points, noms, dates — vous voudrez presque toujours l'égalité de contenu à la place, ce qui implique de redéfinir les deux méthodes ensemble.
Le contrat
equals doit satisfaire quatre règles :
- Réflexive —
x.equals(x)est vrai. - Symétrique —
x.equals(y)si et seulement siy.equals(x). - Transitive — si
x.equals(y)ety.equals(z), alorsx.equals(z). - Cohérente — des appels répétés avec des champs inchangés donnent la même réponse.
De plus : x.equals(null) doit retourner false, jamais lever d'exception.
hashCode a une règle qui le lie à equals :
- Les objets égaux doivent avoir des codes de hachage égaux. Les objets inégaux peuvent partager un code de hachage (les collisions sont autorisées, elles nuisent simplement aux performances).
Cette règle unique explique pourquoi vous ne pouvez pas redéfinir l'une sans l'autre. Si a.equals(b) mais a.hashCode() != b.hashCode(), HashSet les place dans des compartiments différents, contains trouve le mauvais, et vous avez un doublon fantôme.
Observation de la rupture du contrat
Cette classe redéfinit equals mais oublie hashCode, donc elle hérite du hachage basé sur l'identité de Object. Les deux objets sont « égaux » mais se retrouvent dans des compartiments différents — contains ne peut pas trouver celui que vous venez d'ajouter :
equals indique que les objets sont identiques, mais l'ensemble ne peut pas trouver le second. Redéfinissez hashCode de manière cohérente et la recherche réussit.
Anatomie d'un equals correct
Un equals fonctionnel suit une structure standard :
public final class Point {
private final int x;
private final int y;
public Point(int x, int y) { this.x = x; this.y = y; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Point p)) return false;
return x == p.x && y == p.y;
}
@Override
public int hashCode() {
return Objects.hash(x, y);
}
}Étape par étape :
- Court-circuit d'identité.
this == ogère rapidement le cas courant. - Vérification de type avec liaison.
instanceof Point prejette les valeurs nulles et les mauvais types en une seule expression et lie la référence réduite. - Comparaison de champs. Utilisez
==pour les primitives,Objects.equals(a, b)pour les références nullables,Float.compare/Double.comparepour les flottants.
Objects.hash(...) construit un hachage à partir d'une liste de champs. C'est légèrement plus lent que du code XOR/multiplication écrit à la main, mais c'est correct et sans ambiguïté.
getClass ou instanceof ?
Deux approches s'affrontent :
instanceofpermet à une instance de sous-classe d'être égale à une instance parent si l'ensemble de champs comparés est identique. Légèrement plus flexible.getClass()exige la même classe d'exécution exacte. Plus facile à maintenir symétrique dans les hiérarchies, mais rompt la substituabilité.
Pour la plupart des classes de type valeur, la voie la plus simple est de rendre la classe final et d'utiliser instanceof. Sans final, mélanger les deux styles dans une hiérarchie est là où vivent la plupart des bugs d'égalité. Les records contournent entièrement la décision — ils sont implicitement finaux et le equals généré utilise une vérification de type exacte.
Champs à virgule flottante
N'utilisez pas == sur des champs double ou float — la valeur +0.0 est égale à -0.0 avec ==, mais Double.compare les traite différemment, et NaN == NaN est false. Double.compare(a, b) == 0 et Float.compare donnent la réponse cohérente requise par le contrat.
Tableaux
Object.equals sur un tableau compare les références, pas le contenu. Utilisez Arrays.equals(a, b) pour les tableaux unidimensionnels, Arrays.deepEquals pour les multidimensionnels. De même, utilisez Arrays.hashCode / Arrays.deepHashCode dans hashCode.
La mutabilité est hostile aux collections basées sur le hachage
Si vous modifiez un champ qui fait partie de equals/hashCode après avoir placé l'objet dans un HashSet, le compartiment où l'ensemble l'avait placé ne correspond plus au nouveau hachage — et l'objet devient inaccessible via contains. La règle la plus sûre : les champs utilisés dans equals doivent être final. Si ce n'est pas possible, ne placez jamais l'objet dans une collection basée sur le hachage.
N'écrivez aucune des deux méthodes à la main pour les classes de données simples
Si la classe est un simple conteneur de données, préférez un record — le compilateur génère pour vous un equals et un hashCode corrects, et les deux resteront toujours synchronisés à mesure que les champs changent. Si vous ne pouvez pas utiliser un record, la commande « generate equals/hashCode » de votre IDE est la meilleure alternative.
Un exemple complet
La suite
equals permet à vos objets de se comparer ; toString leur permet de se décrire. Le prochain chapitre porte sur la redéfinition de toString pour produire une sortie réellement utile dans les logs, les messages d'erreur et les débogueurs. Continuez vers la méthode toString de Java.