1. Le problème des « classes DTO » : pourquoi avons‑nous besoin des classes record ?
Soyons honnêtes : combien de fois avez-vous écrit une classe comme celle‑ci ?
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 getX() {
return x;
}
public int getY() {
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 Objects.hash(x, y);
}
@Override
public String toString() {
return "Point{" + "x=" + x + ", y=" + y + '}';
}
}
Rien de bien compliqué, mais comptez les lignes ! Et maintenant, imaginez que vous avez 10 classes de ce type, chacune avec 5–6 champs. Même l’IDE se fatigue à générer ce code passe‑partout. Et si vous décidez d’ajouter un nouveau champ — il faudra modifier le constructeur, l’accesseur, equals, hashCode, toString... Ennui, routine et source d’erreurs.
On appelle ces classes des DTO (Data Transfer Object) ou des objets valeur. Elles ne font que stocker des données — c’est tout. Mais à cause du code passe‑partout, leur maintenance est pénible.
Si cela ne vous paraît pas être un problème, attendez simplement le jour où vous devrez modifier 50 de ces classes d’un coup. Ce jour‑là, vous penserez aux classes record avec une tendresse particulière !
2. Introduction aux record : syntaxe et magie de Java 16+
Avec Java 16, tout a changé. Un nouveau type de classe est apparu — record. Elles sont conçues spécialement pour les cas où il faut simplement stocker un ensemble de données. La syntaxe ressemble presque à un tuple dans d’autres langages.
Comment déclarer un record ?
public record Point(int x, int y) { }
Et… c’est tout ! Vous venez de créer une classe immuable avec deux champs, un constructeur, des accesseurs, equals, hashCode et toString. Sans verbiage inutile.
Que fait Java « sous le capot » ?
- Les champs x et y deviennent private final.
- Les accesseurs sont générés automatiquement : int x() et int y().
- Constructeur : public Point(int x, int y).
- equals/hashCode : comparent tous les champs par valeur.
- toString : retourne une chaîne du type "Point[x=1, y=2]".
On peut dire qu’un record est un « DTO sous stéroïdes » : moins de code, plus de garanties, moins de bogues.
Immuabilité (immutability)
Tous les champs d’une classe record sont automatiquement final. Après la création de l’objet, on ne peut plus le modifier — le compilateur le garantit.
Si vous essayez d’ajouter un setter ou de rendre un champ non final, le compilateur vous arrêtera. Une telle sollicitude pour votre tranquillité est rare !
3. Exemple d’utilisation d’une classe record
Classe classique (beaucoup de code) :
public class Client {
private final String name;
private final int id;
public Client(String name, int id) {
this.name = name;
this.id = id;
}
public String getName() { return name; }
public int getId() { return id; }
// equals, hashCode, toString ...
}
Classe record (une seule ligne !) :
public record Client(String name, int id) { }
Utilisation :
public class Main {
public static void main(String[] args) {
Client client = new Client("Ivan", 123);
System.out.println(client.name()); // Ivan
System.out.println(client.id()); // 123
System.out.println(client); // Client[name=Ivan, id=123]
}
}
À noter :
- Les méthodes d’accès portent le même nom que les champs : name(), id().
- Pas de setName() ni de setId() — l’objet ne peut pas être modifié après sa création.
4. Avantages des classes record : moins de code, moins d’erreurs
Moins de code — plus de bonheur
Pourquoi écrire 40 lignes s’il suffit d’une seule ? Les classes record font gagner du temps et des nerfs, surtout dans les grands projets où l’on manipule beaucoup de DTO et d’objets valeur.
Immuabilité « par contrat »
- Les classes record sont toujours final et immuables.
- On ne peut ni falsifier l’objet ni le modifier par inadvertance.
- Pas de bogues « étranges » dus à une modification d’état à un endroit inattendu.
- Peut être utilisé en toute sécurité dans les programmes multithread (si tous les champs sont eux aussi immuables).
Génération automatique de equals/hashCode/toString
Inutile d’écrire à la main les méthodes de comparaison, de calcul de hash et d’affichage lisible. Tout est fait automatiquement et correctement.
Client c1 = new Client("Anna", 42);
Client c2 = new Client("Anna", 42);
System.out.println(c1.equals(c2)); // true
System.out.println(c1.hashCode() == c2.hashCode()); // true
System.out.println(c1); // Client[name=Anna, id=42]
Idéal pour les collections et les clés
Les objets record peuvent être utilisés comme clés dans HashMap, éléments dans HashSet, etc. — tout fonctionnera correctement, car equals et hashCode prennent en compte tous les champs.
import java.util.HashMap;
import java.util.Map;
Map<Client, String> clients = new HashMap<>();
clients.put(new Client("Anna", 42), "VIP");
System.out.println(clients.get(new Client("Anna", 42))); // VIP
Description explicite des données
La syntaxe d’une classe record indique immédiatement quelles données sont stockées et que l’objet est immuable. Cela rend le code plus clair pour les autres développeurs (et pour vous dans six mois).
5. Tableau : comparaison entre classe classique et classe record
| Classe classique | Classe record | |
|---|---|---|
| Syntaxe | Beaucoup de code | Une seule ligne |
| Immuabilité | Doit être écrite explicitement | Garantie par le compilateur |
| Génération automatique des méthodes | Non | Oui (equals, hashCode, toString) |
| Possibilité d’ajouter des champs | Oui | Uniquement les composants du record |
| Héritage | Héritage possible | Toujours final, héritage impossible |
| Utilisation dans les collections | Implémentation correcte des méthodes requise | Fonctionne « prêt à l’emploi » |
6. Faisons évoluer l’application d’apprentissage : exemple avec une classe record
Supposons que, dans votre application bancaire d’apprentissage, vous deviez stocker des opérations sur le compte : date, montant, type d’opération (par exemple, « dépôt » ou « retrait »).
Avant Java 16 :
public class Transaction {
private final LocalDate date;
private final double amount;
private final String type;
public Transaction(LocalDate date, double amount, String type) {
this.date = date;
this.amount = amount;
this.type = type;
}
public LocalDate getDate() { return date; }
public double getAmount() { return amount; }
public String getType() { return type; }
// equals, hashCode, toString ...
}
Avec Java 16 et les record :
import java.time.LocalDate;
public record Transaction(LocalDate date, double amount, String type) { }
Utilisation :
Transaction t = new Transaction(LocalDate.now(), 100.0, "deposit");
System.out.println(t); // Transaction[date=2024-06-01, amount=100.0, type=deposit]
System.out.println(t.amount()); // 100.0
7. Visualisation : ce que génère un record
Regardons une classe record « développée » (approximativement ce que le compilateur génère) :
public final class Point extends java.lang.Record {
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) { /* comparaison par attributs */ }
@Override
public int hashCode() { /* calcul basé sur les attributs */ }
@Override
public String toString() { /* affichage lisible */ }
}
En bref : quand utiliser un record
- Quand vous avez besoin d’un objet immuable avec un ensemble de données.
- Quand vous voulez des equals/hashCode/toString « honnêtes » sans écriture manuelle.
- Quand vous créez des DTO, des objets valeur, des paires, des triplets, une couleur, un point, un intervalle, une clé pour une collection, etc.
8. Erreurs typiques avec les classes record
Erreur n° 1 : tentative d’ajouter un setter ou de modifier un champ après la création.
Une classe record n’autorise pas la modification de ses champs. Si vous essayez d’ajouter une méthode du type setX(int x), le compilateur dira immédiatement « interdit ». Idem si vous tentez de modifier un champ directement.
Erreur n° 2 : tentative d’ajouter un champ non statique.
Dans une classe record, vous ne pouvez déclarer que des composants (les champs indiqués entre parenthèses après le nom du record) et des champs statiques. Les champs d’instance ordinaires sont interdits — le compilateur ne vous laissera pas faire.
Erreur n° 3 : utiliser un record pour une logique mutable.
Les classes record ne sont pas destinées aux objets à état mutable. Si vous devez modifier quelque chose après la création — utilisez une classe classique.
Erreur n° 4 : oublier qu’un record est toujours final.
Une classe record ne peut pas être héritée et ne peut pas servir de super‑classe. Toute tentative de violer cette contrainte conduit à une erreur de compilation. Signe clé : n’essayez pas « d’étendre » un record : il est conçu comme un type immuable achevé.
Erreur n° 5 : ignorer les méthodes auto‑générées.
Si vous redéfinissez equals, hashCode ou toString, soyez prudent — ne cassez pas leur contrat, sinon les collections et les comparaisons fonctionneront mal.
GO TO FULL VERSION