Le pattern matching a profondément simplifié l’écriture de code orienté données en Java. Finalisé dans Java 21 pour switch et les « record patterns », il s’impose désormais comme un outil idiomatique dans le Java moderne.
Objectifs de l’article :
- Comprendre le pattern matching pour
instanceofetswitch - Découvrir les record patterns et leur combinaison avec
switch - Utiliser les
whenet écrire desswitchexhaustifs - Tirer parti des classes
sealedpour des hiérarchies sûres - Connaître les limites, pièges et bonnes pratiques
Pré‑requis : Java 17+ conseillé (LTS). Les fonctionnalités présentées comme « finalisées » le sont dès Java 21.
1) Rappel : pattern matching pour instanceof
Avant Java 16/17, on écrivait :
if (obj instanceof String) {
String s = (String) obj; // cast explicite
System.out.println(s.toUpperCase());
}
Avec le pattern matching pour instanceof :
if (obj instanceof String s) {
System.out.println(s.toUpperCase()); // s est déjà typé
}
- Le binding (
String s) est introduit dans la condition. - Le scope de
sest limité au bloc où le test est vrai.
2) Pattern matching pour switch (Java 21)
Le switch accepte des patterns de type et valeur (when).
Exemple simple :
static String render(Object o) {
return switch (o) {
case null -> "<null>"; // null est géré explicitement
case Integer i -> "int=" + i;
case Long l -> "long=" + l;
case String s when s.isBlank() -> "<empty string>"; // garde
case String s -> "str='" + s + "'";
default -> "autre=" + o.getClass().getSimpleName();
};
}
Points clés :
case nullest possible et utile pour éliminer les NPE.- Les patterns sont testés dans l’ordre, la première correspondance gagne.
- Les
whenaffinent un pattern par une condition booléenne. - Le compilateur vérifie l’exhaustivité (selon les types, notamment avec
sealed).
3) Record patterns (Java 21)
Les records permettent de déstructurer un objet par ses composants, directement dans le case.
record Point(int x, int y) {}
static String quadrant(Object o) {
return switch (o) {
case Point(int x, int y) when x == 0 && y == 0 -> "origin";
case Point(int x, int y) when x >= 0 && y >= 0 -> "Q1";
case Point(int x, int y) when x < 0 && y >= 0 -> "Q2";
case Point(int x, int y) when x < 0 && y < 0 -> "Q3";
case Point(int x, int y) when x >= 0 && y < 0 -> "Q4";
default -> "n/a";
};
}
Point(int x, int y)liex/ysans écrire d’accesseurs explicitement.- Les when permettent d’exprimer la logique métier localement.
Nesting (imbriquer des patterns)
record Line(Point start, Point end) {}
static int manhattan(Object o) {
return switch (o) {
case Line(Point(int x1, int y1), Point(int x2, int y2)) ->
Math.abs(x1 - x2) + Math.abs(y1 - y2);
default -> 0;
};
}
4) sealed + patterns : des hiérarchies fermées et exhaustives
Les classes scellées permettent de contrôler les sous‑types et aident le compilateur à vérifier l’exhaustivité des switch.
sealed interface Shape permits Circle, Rectangle, Triangle {}
record Circle(double r) implements Shape {}
record Rectangle(double w, double h) implements Shape {}
record Triangle(double a, double b, double c) implements Shape {}
static double area(Shape s) {
return switch (s) {
case Circle(double r) -> Math.PI * r * r;
case Rectangle(double w, double h) -> w * h;
case Triangle(double a, double b, double c) -> heron(a, b, c);
}; // exhaustif : pas de default nécessaire
}
Ici, l’absence de default est possible, car la hiérarchie est connue (grâce à sealed). En cas d’ajout d’un nouveau sous‑type autorisé, le compilateur signalera les switch non à jour.
5) Dominance, ordre des case et variable shadowing
- Placez les
caseplus spécifiques avant les plus génériques, sinon les spécifiques deviennent inatteignables.
static String f(Object o) {
return switch (o) {
case String s when s.length() > 10 -> "long string";
case String s -> "string";
case Object x -> "object";
};
}
- Le nom des variables de binding doit être unique par alternative ; évitez les collisions avec des variables existantes dans la portée.
6) Null et switch
case nullest supporté et recommandé siopeut êtrenull.- Sans
case nullnidefault, unswitchsur une référencenulllancerait unNullPointerException.
7) Bonnes pratiques
- Préférez les
switchexpression (switch (...) { ... }) pour des retours clairs et immutables. - Limitez la logique dans les
when. - Combinez
sealed+ records + patterns pour coder des « sum types » lisibles. - Conservez l’exhaustivité : évitez
defaultquand une hiérarchie scellée la rend vérifiable. - Gardez les
casecourts ; extraire en méthodes si nécessaire.
8) Pièges fréquents
- Un
casegénérique (Object o) placé trop tôt capture tout et rend les suivants inaccessibles. - When avec effets de bord : évitez d’appeler des méthodes non idempotentes dans
when. - Ne confondez pas record patterns et déconstruction arbitraire : seuls les records (ou patterns définis) sont déstructurables de cette façon.
- Attention aux
switchnon exhaustifs sur des hiérarchies nonsealed: gardez undefaultsensé.
FAQ
- Peut‑on utiliser les patterns avec des types primitifs ?
- Les patterns de type s’appliquent aux références ; pour les primitifs, on continue d’utiliser les
caselittéraux (case 1, 2, 3 -> ...).
- Les patterns de type s’appliquent aux références ; pour les primitifs, on continue d’utiliser les
- Est‑ce disponible en Java 17 ?
instanceofavec binding fonctionne. Lesswitch/record patterns finalisés arrivent en Java 21. Sur Java 17, certaines fonctionnalités n’existent pas encore.
Conclusion
Le pattern matching apporte des switch plus lisibles, moins de casts et des logiques déclaratives puissantes, surtout combiné avec records et sealed. Finalisé en Java 21, c’est un incontournable du Java moderne.
Pour aller plus loin
- JEP 441 : Pattern Matching for switch (Final, JDK 21)
- JEP 440 : Record Patterns (Final, JDK 21)
- JEP 409 : Sealed Classes (Final, JDK 17)
- Javadoc :
switchexpressions et statements (Java 21+)