Filtrage par motif en Java
Utilisez le filtrage par motif en Java pour instanceof et switch — motifs de type, motifs de record et déconstruction.
Pendant des années, le code Java travaillant avec des valeurs de type inconnu suivait un rituel fastidieux : tester le type avec instanceof, puis effectuer un cast vers ce type, puis l'utiliser. Le filtrage par motif compresse ce rituel en une seule expression. Un motif décrit la forme d'une donnée ; si une valeur correspond, Java lie ses parties à des variables utilisables immédiatement — aucun cast manuel n'est requis.
Le filtrage par motif est arrivé par étapes : d'abord les motifs instanceof, puis les motifs dans switch, puis les motifs de record qui déconstruisent les records en leurs composants. Ensemble, ils permettent d'écrire du code déclaratif et sûr au niveau des types, qui reflète la structure des données qu'il manipule.
Ce chapitre couvre le motif instanceof, les motifs de type dans switch, les motifs gardés et la gestion de null, et les motifs de record — puis les réunit dans un programme exécutable. Il s'appuie sur trois fonctionnalités qu'il peut être utile de revoir en premier : l'opérateur instanceof, les records et les expressions switch.
Filtrage par motif pour instanceof
Le schéma classique test-et-cast nécessitait trois références au même type. Le motif instanceof lie une variable au même moment que le test, et la liaison est accessible partout où le test est connu pour être vrai.
Object value = "hello";
// Old way: test, then cast
if (value instanceof String) {
String s = (String) value;
System.out.println(s.length());
}
// Pattern way: test and bind together
if (value instanceof String s) {
System.out.println(s.length());
}Comme la variable liée participe à l'expression booléenne, vous pouvez continuer à affiner dans le même if. Le compilateur prouve que s est sûre à utiliser :
if (value instanceof String s && s.length() > 3) {
System.out.println(s.toUpperCase());
}Motifs dans switch
Un switch peut correspondre à des motifs de type, en distribuant selon le type d'exécution du sélecteur. Chaque case lie la valeur correspondante, de sorte que le corps travaille directement avec une variable typée. Cela transforme de longues chaînes if/else instanceof en un tableau compact et lisible.
static String format(Object value) {
return switch (value) {
case Integer i -> "int: " + i;
case Long l -> "long: " + l;
case String s -> "string: " + s;
default -> "other: " + value;
};
}Un switch à motifs de type doit être exhaustif — il doit couvrir toutes les entrées possibles. Pour les sélecteurs Object arbitraires, cela signifie une branche default ; pour les hiérarchies sealed, le compilateur connaît l'ensemble complet des sous-types et peut vérifier l'exhaustivité sans default.
Motifs gardés et null
Une clause when ajoute une condition booléenne à un case, permettant à deux valeurs du même type d'emprunter des branches différentes. C'est ce qu'on appelle un motif gardé, et l'ordre est important : les cas gardés plus spécifiques viennent avant le cas non gardé de secours.
static String size(String s) {
return switch (s) {
case String t when t.isEmpty() -> "empty";
case String t when t.length() < 5 -> "short";
case String t -> "long (" + t.length() + ")";
};
}Traditionnellement, un switch lançait une NullPointerException sur un sélecteur null. Un switch à motifs peut gérer null explicitement avec un case null, gardant la vérification du null à l'intérieur du même construct plutôt qu'une garde séparée avant lui.
| Fonctionnalité | Syntaxe | Rôle |
|---|---|---|
| Motif de type | case String s | Correspondre par type et lier |
| Motif gardé | case String s when s.isEmpty() | Ajouter une condition à un cas |
| Étiquette null | case null | Correspondre à un sélecteur null |
| Motif de record | case Point(int x, int y) | Déconstruire un record |
Motifs de record
Un motif de record correspond à un record et lie ses composants en une seule opération, évitant ainsi les appels aux accesseurs. Comme les records exposent leurs composants, le compilateur connaît la forme exacte et vous permet de nommer chaque partie en ligne. Les motifs de record s'imbriquent, vous permettant ainsi de destructurer un record de records.
record Point(int x, int y) {}
record Line(Point start, Point end) {}
static String render(Object o) {
return switch (o) {
case Point(int x, int y) -> "point " + x + "," + y;
// Nested: pull both endpoints' coordinates out at once
case Line(Point(int x1, int y1), Point(int x2, int y2)) ->
"line " + x1 + "," + y1 + " -> " + x2 + "," + y2;
default -> "unknown";
};
}Le filtrage par motif excelle avec les types scellés : lorsqu'une interface liste ses implémentations autorisées, un switch sur celles-ci est exhaustif sans default, et l'ajout d'un nouveau sous-type transforme le cas manquant en erreur de compilation plutôt qu'en bug silencieux.
Un exemple complet et exécutable
Le programme ci-dessous réunit tous les éléments. Il utilise un motif instanceof avec une garde, une hiérarchie Shape scellée de records, des motifs de record qui déconstruisent chaque forme dans un switch, un motif gardé qui détecte un carré, et un case null — le tout sans un seul cast explicite.
Ce que l'on retient de l'exécution :
describe(42)affichepositive int 42parce que la gardeinstanceof Integer i && i > 0teste le type et la valeur ensemble avant de lieri.describe(-5)tombe dansunknown— le même motifIntegercorrespond au type, mais la gardei > 0échoue, montrant comment une garde affine un motif de type.- Le
switchdearean'a pas besoin dedefault:Shapeest scellé, donc listerCircle,RectangleetTriangleest exhaustif et le compilateur est satisfait. - Le rectangle
5.0 x 5.0s'affiche commesquare side=5.0parce que son cas gardéwhen w == hest placé avant le cas généralRectangle ret l'emporte. - La dernière ligne affiche
no shape: la branchecase nullgère un sélecteurnullà l'intérieur du switch au lieu de lancer uneNullPointerException.