JEP 433 - Pattern Matching for switch
Pattern matching dans les expressions switch pour un code plus expressif et sûr
En résumé
Le pattern matching pour switch permet de tester une valeur contre plusieurs patterns (motifs) de manière expressive et sûre. Au lieu de simples constantes, vous pouvez maintenant utiliser des types, des conditions (guards) et déstructurer des objets. Le compilateur garantit l'exhaustivité des cas et évite les erreurs courantes.
Contexte et motivation
Le switch classique et ses limites
Historiquement, le switch Java était limité aux types primitifs et aux constantes :
// Switch classique : uniquement des constantes
String jour = "lundi";
switch (jour) {
case "lundi":
case "mardi":
System.out.println("Début de semaine");
break;
case "vendredi":
System.out.println("Presque le weekend");
break;
default:
System.out.println("Autre jour");
}
Pour traiter différents types, on devait utiliser instanceof avec des if-else :
// Avant : code verbeux et répétitif
static String formater(Object obj) {
String resultat;
if (obj instanceof Integer i) {
resultat = String.format("Entier : %d", i);
} else if (obj instanceof String s) {
resultat = String.format("Texte : %s", s);
} else if (obj instanceof Double d) {
resultat = String.format("Décimal : %.2f", d);
} else {
resultat = obj.toString();
}
return resultat;
}
Solution : pattern matching dans switch
Avec la JEP 433, on peut utiliser des type patterns directement dans le switch :
// Après : code concis et expressif
static String formater(Object obj) {
return switch (obj) {
case Integer i -> String.format("Entier : %d", i);
case String s -> String.format("Texte : %s", s);
case Double d -> String.format("Décimal : %.2f", d);
default -> obj.toString();
};
}
Fonctionnalités principales
1. Type Patterns (motifs de type)
Testez le type et capturez la valeur en une seule ligne :
static double calculerAire(Shape shape) {
return switch (shape) {
case Circle c -> Math.PI * c.rayon() * c.rayon();
case Rectangle r -> r.largeur() * r.hauteur();
case Triangle t -> 0.5 * t.base() * t.hauteur();
default -> throw new IllegalArgumentException("Forme inconnue");
};
}
2. Guarded Patterns (motifs gardés)
Ajoutez des conditions avec when :
static String evaluerNote(int note) {
return switch (note) {
case int n when n >= 90 -> "Excellent";
case int n when n >= 75 -> "Bien";
case int n when n >= 60 -> "Assez bien";
case int n when n >= 50 -> "Passable";
default -> "Insuffisant";
};
}
// Exemple plus complexe avec objets
static String categoriserClient(Client client) {
return switch (client) {
case Client c when c.age() < 18 -> "Mineur";
case Client c when c.age() >= 65 -> "Senior";
case Client c when c.estPremium() -> "Client Premium";
case Client c when c.commandes() > 100 -> "Client fidèle";
default -> "Client standard";
};
}
3. Null handling (gestion de null)
Vous pouvez maintenant gérer null explicitement dans le switch :
static String decrire(Object obj) {
return switch (obj) {
case null -> "Valeur nulle";
case String s -> "Texte : " + s;
case Integer i -> "Nombre : " + i;
default -> "Autre : " + obj;
};
}
// Avant Java 21, ceci lèverait une NullPointerException
4. Exhaustivité (completeness)
Le compilateur vérifie que tous les cas sont couverts, surtout avec les sealed classes :
sealed interface Resultat permits Succes, Erreur {}
record Succes(String donnee) implements Resultat {}
record Erreur(String message) implements Resultat {}
// Le compilateur vérifie l'exhaustivité : pas besoin de default !
static String traiter(Resultat resultat) {
return switch (resultat) {
case Succes s -> "OK : " + s.donnee();
case Erreur e -> "Erreur : " + e.message();
// Pas de 'default' nécessaire car tous les cas sont couverts
};
}
Exemples pratiques
Exemple 1 : Validation de formulaire
record FormulaireValide(String nom, String email) {}
record FormulaireInvalide(List erreurs) {}
static String afficherResultat(Object resultat) {
return switch (resultat) {
case FormulaireValide v ->
"Formulaire valide pour " + v.nom();
case FormulaireInvalide inv when inv.erreurs().size() == 1 ->
"Une erreur : " + inv.erreurs().get(0);
case FormulaireInvalide inv ->
"Plusieurs erreurs : " + String.join(", ", inv.erreurs());
default ->
"Type de résultat inattendu";
};
}
Exemple 2 : Traitement JSON simplifié
sealed interface JsonValue {}
record JsonString(String value) implements JsonValue {}
record JsonNumber(double value) implements JsonValue {}
record JsonBoolean(boolean value) implements JsonValue {}
record JsonNull() implements JsonValue {}
record JsonArray(List items) implements JsonValue {}
record JsonObject(Map fields) implements JsonValue {}
static String convertirEnTexte(JsonValue json) {
return switch (json) {
case JsonString s -> "\"" + s.value() + "\"";
case JsonNumber n -> String.valueOf(n.value());
case JsonBoolean b -> String.valueOf(b.value());
case JsonNull _ -> "null";
case JsonArray a -> "[" + a.items().size() + " éléments]";
case JsonObject o -> "{" + o.fields().size() + " champs}";
};
}
Exemple 3 : Machine à états
sealed interface Etat permits EnAttente, EnCours, Termine, Erreur {}
record EnAttente() implements Etat {}
record EnCours(int progression) implements Etat {}
record Termine(String resultat) implements Etat {}
record Erreur(Exception ex) implements Etat {}
static String afficherEtat(Etat etat) {
return switch (etat) {
case EnAttente _ -> "⏳ En attente de démarrage";
case EnCours e when e.progression() < 50 -> "🔄 En cours (" + e.progression() + "%)";
case EnCours e -> "🔄 Presque fini (" + e.progression() + "%)";
case Termine t -> "✅ Terminé : " + t.resultat();
case Erreur e -> "❌ Erreur : " + e.ex().getMessage();
};
}
Exemple 4 : Analyse syntaxique
sealed interface Expression {}
record Nombre(int valeur) implements Expression {}
record Addition(Expression gauche, Expression droite) implements Expression {}
record Multiplication(Expression gauche, Expression droite) implements Expression {}
static int evaluer(Expression expr) {
return switch (expr) {
case Nombre n -> n.valeur();
case Addition add ->
evaluer(add.gauche()) + evaluer(add.droite());
case Multiplication mult ->
evaluer(mult.gauche()) * evaluer(mult.droite());
};
}
// Utilisation
Expression calcul = new Addition(
new Nombre(5),
new Multiplication(new Nombre(3), new Nombre(4))
);
int resultat = evaluer(calcul); // 5 + (3 * 4) = 17
Combinaison avec d'autres fonctionnalités
Avec les Records
record Point(int x, int y) {}
static String localiser(Point p) {
return switch (p) {
case Point(0, 0) -> "Origine";
case Point(int x, 0) -> "Sur l'axe X en " + x;
case Point(0, int y) -> "Sur l'axe Y en " + y;
case Point(int x, int y) when x == y -> "Sur la diagonale en (" + x + "," + y + ")";
case Point(int x, int y) -> "Point quelconque (" + x + "," + y + ")";
};
}
Avec les Sealed Classes
sealed interface Reponse {}
record Succes(String data) implements Reponse {}
record ErreurClient(int code, String message) implements Reponse {}
record ErreurServeur(Exception ex) implements Reponse {}
static String traiterReponse(Reponse reponse) {
return switch (reponse) {
case Succes s -> "Données reçues : " + s.data();
case ErreurClient(int code, String msg) when code == 404 ->
"Ressource non trouvée";
case ErreurClient(int code, String msg) ->
"Erreur client " + code + " : " + msg;
case ErreurServeur e ->
"Erreur serveur : " + e.ex().getMessage();
};
}
Avantages
Pour le développeur
- ✅ Code plus concis et lisible
- ✅ Moins de code boilerplate
- ✅ Sécurité accrue (exhaustivité vérifiée)
- ✅ Réduction des bugs (pas de fall-through accidentel)
Pour le compilateur
- ✅ Vérification d'exhaustivité des cas
- ✅ Détection de code mort (unreachable code)
- ✅ Optimisations possibles
Bonnes pratiques
✅ À faire
- Préférer les expressions switch (avec
->) aux statements - Utiliser les sealed classes pour garantir l'exhaustivité
- Combiner avec les records pour plus de clarté
- Gérer explicitement le cas
nullsi nécessaire - Ordonner les cas du plus spécifique au plus général
❌ À éviter
- Éviter des guards trop complexes (mieux vaut extraire dans une méthode)
- Ne pas mélanger l'ancien et le nouveau style dans le même switch
- Éviter les side effects dans les cases (préférer du code pur)
Migration depuis l'ancien code
// Ancien style (avant Java 21)
String description;
if (obj instanceof String s) {
description = "Texte de longueur " + s.length();
} else if (obj instanceof Integer i) {
description = "Nombre " + i;
} else if (obj instanceof List> list) {
description = "Liste de " + list.size() + " éléments";
} else {
description = "Type inconnu";
}
// Nouveau style (Java 21+)
String description = switch (obj) {
case String s -> "Texte de longueur " + s.length();
case Integer i -> "Nombre " + i;
case List> l -> "Liste de " + l.size() + " éléments";
default -> "Type inconnu";
};