1. Approfondissons les classes record
Avant de plonger dans les record patterns, rappelons ce qu’est une classe record.
Un record est une classe compacte et immuable avec des méthodes equals, hashCode, toString générées automatiquement, ainsi que la création automatique d’accesseurs pour tous les composants. Les classes record sont apparues dans Java 16 et ont été un vrai cadeau pour tous ceux qui en avaient assez d’écrire des DTO à la main.
// Classique : Point avec deux champs
record Point(int x, int y) {}
Création d’un objet et accès aux composants :
Point p = new Point(10, 20);
System.out.println(p.x()); // 10
System.out.println(p.y()); // 20
System.out.println(p); // Point[x=10, y=20]
Pourquoi utiliser les record patterns
Avant Java 21, si vous vouliez vérifier qu’un objet est un record et en extraire les champs, il fallait écrire beaucoup de code manuel : vérification de type via instanceof, transtypage et appel des accesseurs.
Object obj = new Point(5, 7);
if (obj instanceof Point) {
Point p = (Point) obj;
int x = p.x();
int y = p.y();
System.out.println("x=" + x + ", y=" + y);
}
Si le record est complexe ou imbriqué, le code se transforme vite en « forêt de décompressions ». Et l’utiliser dans un switch était auparavant peu pratique.
Les record patterns permettent de décomposer les valeurs des classes record directement dans le pattern matching : aussi bien dans instanceof que dans switch. Cela rend le code plus court, plus sûr et plus lisible.
2. Syntaxe des record patterns : l’exemple le plus simple
record Point(int x, int y) {}
Object obj = new Point(10, 20);
if (obj instanceof Point(int x, int y)) {
System.out.println("x=" + x + ", y=" + y);
}
Que se passe-t-il ici ?
- obj instanceof Point(int x, int y) — on vérifie le type et on décompose immédiatement les composants dans les variables x et y.
- Les variables x et y ne sont accessibles qu’à l’intérieur du bloc if.
Résultat :
x=10, y=20
Attention : si obj n’est pas un Point ou vaut null, le bloc if ne s’exécutera pas et les variables ne seront pas déclarées.
3. Record patterns dans switch : concision et sécurité
record Point(int x, int y) {}
record Circle(Point center, int radius) {}
Object shape = new Point(3, 4);
switch (shape) {
case Point(int x, int y) -> System.out.println("Point: x=" + x + ", y=" + y);
case Circle(Point center, int r) -> System.out.println("Circle with radius " + r);
default -> System.out.println("Unknown shape!");
}
- Dans le case Point(int x, int y), on obtient immédiatement x et y.
- Dans le case Circle(Point center, int r), center et r sont disponibles.
Résultat :
Point: x=3, y=4
Avec les hiérarchies sealed, le switch devient exhaustif : si vous oubliez de traiter un cas, le compilateur vous en avertira.
4. Record patterns imbriqués : décomposition dans la décomposition
record Point(int x, int y) {}
record Line(Point start, Point end) {}
Object obj = new Line(new Point(1, 2), new Point(3, 4));
if (obj instanceof Line(Point(int x1, int y1), Point(int x2, int y2))) {
System.out.println("Line from (" + x1 + "," + y1 + ") to (" + x2 + "," + y2 + ")");
}
Résultat :
Line from (1,2) to (3,4)
Vous travaillez avec des arbres, des graphes, des structures complexes ? Les patterns imbriqués sont vos alliés.
5. Record patterns avec expressions de garde (when)
Object obj = new Point(0, 100);
if (obj instanceof Point(int x, int y) && x == 0) {
System.out.println("Point sur l'axe Y : y=" + y);
}
Dans un switch, on utilise when :
switch (obj) {
case Point(int x, int y) when x == 0 -> System.out.println("Sur l'axe Y : y=" + y);
case Point(int x, int y) when y == 0 -> System.out.println("Sur l'axe X : x=" + x);
case Point(int x, int y) -> System.out.println("Point ordinaire");
default -> System.out.println("Pas un point");
}
6. Application des record patterns dans une application réelle
Prenons un exemple pratique — des figures géométriques.
record Point(int x, int y) {}
record Circle(Point center, int radius) {}
record Rectangle(Point topLeft, int width, int height) {}
public static void printShapeInfo(Object shape) {
switch (shape) {
case Point(int x, int y) -> System.out.println("Point: (" + x + ", " + y + ")");
case Circle(Point(int x, int y), int radius) ->
System.out.println("Circle: center=(" + x + ", " + y + "), radius=" + radius);
case Rectangle(Point(int x, int y), int width, int height) ->
System.out.println("Rectangle: topLeft=(" + x + ", " + y + "), size=" + width + "x" + height);
default -> System.out.println("Unknown shape");
}
}
Exemple d’appel :
printShapeInfo(new Rectangle(new Point(5, 10), 20, 30));
Résultat :
Rectangle: topLeft=(5, 10), size=20x30
7. Limitations et subtilités d’utilisation des record patterns
Uniquement pour les classes record. Les record patterns ne fonctionnent qu’avec des objets qui sont réellement des classes record.
class NotARecord {
int a, b;
NotARecord(int a, int b) { this.a = a; this.b = b; }
}
// Erreur ! Pas un record
// if (obj instanceof NotARecord(int a, int b)) { ... }
Correspondance du nombre et du type des composants. Le pattern doit correspondre aux composants de la classe record.
record Pair(int a, String b) {}
// Erreur : les types ne correspondent pas
// if (obj instanceof Pair(String a, int b)) { ... }
Variables accessibles uniquement à l’intérieur du bloc. Les noms déclarés dans le pattern sont visibles dans le corps du if ou du case spécifique où la correspondance a réussi.
8. Patterns imbriqués : exemple avec un arbre
sealed interface Expr permits NumberExpr, PlusExpr, MinusExpr {}
record NumberExpr(int value) implements Expr {}
record PlusExpr(Expr left, Expr right) implements Expr {}
record MinusExpr(Expr left, Expr right) implements Expr {}
int eval(Expr expr) {
return switch (expr) {
case NumberExpr(int value) -> value;
case PlusExpr(Expr left, Expr right) -> eval(left) + eval(right);
case MinusExpr(Expr left, Expr right) -> eval(left) - eval(right);
};
}
Exemple d’utilisation :
Expr e = new PlusExpr(new NumberExpr(7), new MinusExpr(new NumberExpr(10), new NumberExpr(3)));
System.out.println(eval(e)); // 7 + (10 - 3) = 14
Ici, nous décomposons directement NumberExpr(int value), PlusExpr(Expr left, Expr right) et MinusExpr(Expr left, Expr right) — le code devient concis et expressif.
9. Tableau : comparaison du pattern matching avec et sans record patterns
| Méthode | Conversion de type manuelle | Pattern matching avec des record patterns |
|---|---|---|
| Vérification du type | |
|
| Conversion de type | |
Pas nécessaire |
| Accès aux composants | |
|
| Décomposition imbriquée | Beaucoup de code manuel | Patterns imbriqués |
| Utilisation dans switch | Peu pratique/impossible | Simple, concis |
10. Erreurs courantes lors de l’utilisation des record patterns
Erreur n° 1 : tenter d’appliquer un record pattern à une classe qui n’est pas un record. Si vous avez écrit une classe ordinaire et tentez ensuite de la « décomposer » comme un record, le compilateur l’interdira. Les record patterns ne fonctionnent qu’avec de véritables classes record.
Erreur n° 2 : discordance du nombre ou du type des composants. Le pattern doit correspondre exactement à la signature des composants du record. Par exemple, pour record Pair(int a, String b), vous ne pouvez pas écrire Pair(int x, int y).
Erreur n° 3 : utiliser des variables du pattern en dehors du bloc. Les noms déclarés dans le pattern ne sont accessibles qu’à l’intérieur du if/case correspondant. En dehors du bloc, ces variables n’existent pas.
Erreur n° 4 : utiliser des record patterns sur d’anciennes versions du JDK. La prise en charge est apparue dans Java 21. Sur Java 17 et antérieures, vous obtiendrez une erreur de syntaxe. Vérifiez la version du JDK et la prise en charge dans l’IDE.
Erreur n° 5 : patterns trop profondément imbriqués. Les possibilités d’imbrication sont puissantes, mais n’ajoutez pas de complexité inutilement : une structure trop profonde nuit à la lisibilité et augmente le risque d’erreurs.
GO TO FULL VERSION