1. Génération automatique de equals, hashCode, toString
À quoi servent ces méthodes ?
En travaillant avec des objets en Java, vous vous heurtez très vite aux mêmes tâches. Parfois, il faut vérifier si deux objets sont égaux. Par exemple, déterminer s’il est déjà présent dans une collection comme un Set ou une Map. Dans d’autres cas, l’objet est utilisé comme clé dans une HashMap, et là, impossible de s’en sortir sans règles de comparaison spécifiques. Et puis, on veut presque toujours imprimer un objet dans les logs ou à l’écran de manière lisible, plutôt qu’un charabia du type MyClass@7b23ec81.
Pour ces cas, chaque classe en Java dispose de trois méthodes particulières :
- equals(Object o) gère la vérification d’égalité.
- hashCode() donne à l’objet une « empreinte » numérique utile aux collections de type table de hachage.
- toString() retourne une représentation textuelle pratique de l’objet, ce qui facilite grandement le débogage et l’impression.
Pourquoi est-ce pénible dans les classes classiques ?
Dans les classes ordinaires, il faut écrire ces méthodes à la main. Et c’est là que commencent l’ennui et le mal de tête. On obtient une tonne de code passe-partout qui encombre la classe. Il est très facile de se tromper quelque part : oublier de comparer un champ, mal calculer le hashCode et se retrouver avec des bugs mystérieux. Et si vous ajoutez un nouveau champ à la classe, il faudra retourner dans toutes ces méthodes et tout réécrire.
Exemple d’une classe classique
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int x() { return x; }
public int y() { return y; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Point point = (Point) o;
return x == point.x && y == point.y;
}
@Override
public int hashCode() {
return 31 * x + y;
}
@Override
public String toString() {
return "Point[x=" + x + ", y=" + y + "]";
}
}
Ça vous dit quelque chose ? Oui, et ce n’est que pour deux champs ! Et s’il y en a vingt ?
Comment un record s’en charge
Une classe record fait tout cela pour vous. Déclarez simplement :
public record Point(int x, int y) { }
Et Java génèrera automatiquement :
- Un constructeur
- Des accesseurs (x(), y())
- equals, hashCode, toString
Méthodes générées automatiquement
- equals compare toutes les composantes du record par valeur.
- hashCode est calculé à partir de toutes les composantes.
- toString retourne une chaîne du type Point[x=1, y=2].
Voyons-le en action !
public record Point(int x, int y) {}
public class Demo {
public static void main(String[] args) {
Point p1 = new Point(1, 2);
Point p2 = new Point(1, 2);
System.out.println(p1.equals(p2)); // true
System.out.println(p1.hashCode() == p2.hashCode()); // true
System.out.println(p1); // Point[x=1, y=2]
}
}
Sortie :
true
true
Point[x=1, y=2]
Tout fonctionne comme prévu, sans une seule ligne de code superflue !
2. Pourquoi c’est important : collections, débogage et sécurité
Fonctionnement correct dans les collections
Imaginez que vous utilisiez des objets comme clés dans une HashMap ou comme éléments dans un HashSet. Si equals et hashCode sont mal implémentés, les collections auront un comportement étrange : elles ne retrouveront pas l’élément que vous venez d’ajouter ou, au contraire, considéreront deux objets différents comme identiques.
Avec les classes record, vous pouvez être serein : la comparaison et le hachage prennent toujours en compte toutes les composantes du record (dans l’ordre où elles sont déclarées).
Exemple : utiliser un record comme clé
import java.util.HashMap;
import java.util.Map;
public class Demo {
public static void main(String[] args) {
record Point(int x, int y) {}
Map<Point, String> map = new HashMap<>();
Point p1 = new Point(3, 4);
map.put(p1, "Hello!");
Point p2 = new Point(3, 4);
System.out.println(map.get(p2)); // "Hello!" — fonctionne !
}
}
Remarquez que p1 et p2 sont des objets différents (références différentes), mais contiennent les mêmes valeurs de champs, et sont donc considérés égaux. Vous en saurez plus sur Map et HashMap au niveau 26 :P
Praticité du débogage et de la journalisation
Au lieu d’un terne Point@1a2b3c4d (comme c’est le cas par défaut pour les classes ordinaires), une classe record s’imprime de façon claire et informative :
Point[x=3, y=4]
Cela fait gagner beaucoup de temps lors du débogage et de la journalisation.
3. Comment equals, hashCode, toString fonctionnent à l’intérieur d’un record
La méthode equals
Une classe record implémente equals de sorte que deux objets sont considérés égaux si :
- Ils sont du même type (même classe record)
- Tous leurs composants sont égaux (== pour les types primitifs, equals() pour les objets)
Exemple de comparaison
Point p1 = new Point(1, 2);
Point p2 = new Point(1, 2);
Point p3 = new Point(1, 3);
System.out.println(p1.equals(p2)); // true
System.out.println(p1.equals(p3)); // false
La méthode hashCode
Le code de hachage est calculé à partir de toutes les composantes du record, généralement via la méthode standard Objects.hash(...).
System.out.println(p1.hashCode()); // Par exemple, 994
System.out.println(p2.hashCode()); // Également 994
System.out.println(p3.hashCode()); // Un autre nombre
La méthode toString
La représentation textuelle est toujours au format :
ClassName[field1=value1, field2=value2, ...]
System.out.println(p1); // Point[x=1, y=2]
4. Redéfinir equals, hashCode, toString : quand et comment ?
Parfois (rarement, mais cela arrive), il faut modifier le comportement standard de ces méthodes. Par exemple, vous souhaitez que toString retourne une chaîne dans un autre format, ou que la comparaison ne porte que sur une partie des champs.
Attention : si vous redéfinissez equals/hashCode, faites-le en toute connaissance de cause ! Violer leur « contrat » peut engendrer des bugs difficiles à traquer.
Comment redéfinir une méthode
Déclarez simplement votre méthode dans le corps de la classe record :
public record Point(int x, int y) {
@Override
public String toString() {
return "(" + x + "; " + y + ")";
}
}
Point p = new Point(3, 5);
System.out.println(p); // (3; 5)
Peut-on redéfinir equals/hashCode ?
Oui, mais c’est fortement déconseillé si vous n’êtes pas sûr de ce que vous faites. Par exemple, si vous voulez que la comparaison ne se fasse que sur le champ x (ce qui est déjà étrange) :
public record Point(int x, int y) {
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Point other)) return false;
return x == other.x;
}
@Override
public int hashCode() {
return Integer.hashCode(x);
}
}
Point p1 = new Point(1, 2);
Point p2 = new Point(1, 999);
System.out.println(p1.equals(p2)); // true (!)
Mais soyez prudent : si vous redéfinissez equals, redéfinissez toujours aussi hashCode — sinon les collections fonctionneront de manière incorrecte.
Bonnes pratiques
- Si vous ne savez pas précisément pourquoi redéfinir — ne redéfinissez pas !
- Pour toString — vous pouvez sans problème choisir votre propre format si vous le souhaitez.
- Pour equals/hashCode — uniquement s’il y a une bonne raison et que vous en comprenez les conséquences.
5. Pratique : comparaison d’objets et utilisation de record dans les collections
Exemple : comparaison de deux objets record
public record User(String name, int age) {}
public class Demo {
public static void main(String[] args) {
User u1 = new User("Alice", 20);
User u2 = new User("Alice", 20);
User u3 = new User("Bob", 25);
System.out.println(u1.equals(u2)); // true
System.out.println(u1.equals(u3)); // false
System.out.println(u1.hashCode() == u2.hashCode()); // true
System.out.println(u1); // User[name=Alice, age=20]
}
}
Exemple : utiliser un record comme clé dans une HashMap
Imaginons une application où nous stockons le nombre de visites des utilisateurs par leur nom et leur âge (on ne sait jamais, il peut y avoir au club deux « Ivan, 20 ans »).
import java.util.HashMap;
import java.util.Map;
public class Demo {
public static void main(String[] args) {
record User(String name, int age) {}
Map<User, Integer> visits = new HashMap<>();
User ivan20 = new User("Ivan", 20);
User ivan22 = new User("Ivan", 22);
visits.put(ivan20, 5);
visits.put(ivan22, 2);
// Vérifions que la recherche par valeur fonctionne correctement
System.out.println(visits.get(new User("Ivan", 20))); // 5
System.out.println(visits.get(new User("Ivan", 22))); // 2
}
}
Si equals et hashCode n’étaient pas correctement implémentés, la recherche ne fonctionnerait pas. Vous en apprendrez davantage sur Map et HashMap dans les cours du niveau 26 :P
6. Erreurs typiques lors de l’utilisation de equals, hashCode, toString dans les classes record
Erreur n° 1 : Penser que les champs peuvent être modifiés après la création.
Les champs d’un record sont toujours final, et la comparaison se fait sur leurs valeurs définies dans le constructeur. Si, d’une manière détournée, vous « modifiez » l’état interne (par exemple via un objet mutable à l’intérieur d’un champ), l’égalité et le hachage peuvent devenir incorrects.
Erreur n° 2 : Redéfinir equals et oublier hashCode.
Si vous redéfinissez l’une de ces méthodes, redéfinissez toujours l’autre ! Sinon les collections (HashSet, HashMap) auront un comportement imprévisible.
Erreur n° 3 : S’attendre à un autre format pour toString.
Si vous avez besoin d’un format particulier pour la chaîne — redéfinissez simplement toString. Par défaut, le format est toujours ClassName[field1=value1, field2=value2].
Erreur n° 4 : Utiliser un record pour des classes complexes avec des champs mutables.
Les champs d’un record doivent être immuables. Si, comme champ, vous utilisez par exemple une ArrayList et que quelqu’un en modifie le contenu, l’égalité et le code de hachage peuvent « se casser ». Pour un record, privilégiez uniquement des types immuables.
Erreur n° 5 : Utiliser un record pour des classes dont le comportement n’est pas celui d’un value object.
Un record n’est pas « une petite classe à la syntaxe courte ». C’est un value object, conçu pour stocker un ensemble de valeurs. Si vous avez une logique complexe, un état mutable ou un besoin d’héritage, utilisez une classe classique.
GO TO FULL VERSION