Guide complet : Pattern Matching en Java et bonnes pratiques OO¶
1. Introduction¶
Le pattern matching est une fonctionnalité puissante introduite en Java pour simplifier la manipulation des objets en fonction de leur type ou de leur structure. Cependant, son utilisation doit être réfléchie pour respecter les principes du bon design orienté objet (OO). Ce guide explique quand et comment utiliser le pattern matching, ainsi que ses alternatives et limites.
2. Historique et versions¶
| Version | Fonctionnalité introduite |
|---|---|
| Java 16 | Pattern Matching pour instanceof (JEP 394) |
| Java 17 | Pattern Matching pour switch (preview) |
| Java 19 | Record Patterns (preview) |
| Java 21 | Pattern Matching pour switch (finalisé), Record Patterns (standard), Pattern Matching dans les boucles for |
3. Pattern Matching de base : instanceof¶
a. Avant Java 16¶
Pour vérifier le type d’un objet et le caster, il fallait écrire :
b. Depuis Java 16 : Pattern Matching pour instanceof¶
La syntaxe est simplifiée et plus sûre :
if(obj instanceofString s){
System.out.println(s.length()); // s est automatiquement casté et disponible dans le bloc
}
- Avantages :
- Pas besoin de caster manuellement.
- La variable
sest disponible uniquement dans le blocif. - Moins de risques d’erreurs de cast.
c. Exemple avec une hiérarchie de classes¶
interface Shape {
}
record Circle(double radius) implements Shape {
}
record Rectangle(double length, double width) implements Shape {
}
void printArea(Shape shape) {
if (shape instanceof Circle c) {
System.out.println("Aire du cercle : " + Math.PI * c.radius() * c.radius());
} else if (shape instanceof Rectangle r) {
System.out.println("Aire du rectangle : " + r.length() * r.width());
}
}
4. Pattern Matching avec switch (depuis Java 17+)¶
Le pattern matching étend la puissance de l’instruction switch en permettant de :
- Faire correspondre des types.
- Extraire des composants (notamment avec les
record). - Utiliser des motifs (
pattern) directement dans lescase.
a. Syntaxe de base¶
String description = switch (obj) {
case Integer i -> "Nombre entier : " + i;
case String s -> "Chaîne de caractères : " + s;
case Double d -> "Nombre décimal : " + d;
default -> "Type inconnu";
};
- Points clés :
- Le type est vérifié et la variable est automatiquement castée.
- La flèche (
->) est utilisée pour les expressions (pas debreaknécessaire si c’est une expression). - Le
defaultreste obligatoire pour couvrir tous les cas.
b. Utilisation avec des record¶
Le pattern matching brille particulièrement avec les record, car il permet de déconstruire directement leurs
composants :
record Point(int x, int y) {
}
void printCoord(Object obj) {
switch (obj) {
case Point(int x, int y) -> System.out.printf("Point(%d, %d)%n", x, y);
case null -> System.out.println("Objet null");
default -> System.out.println("Type inconnu");
}
}
- Exemple avancé :
record Person(String name, int age) {} void greet(Object obj) { switch (obj) { case Person p when p.age() >= 18 -> System.out.println("Bonjour, " + p.name() + " (adulte)"); case Person p -> System.out.println("Bonjour, " + p.name() + " (mineur)"); case null -> System.out.println("Aucune personne"); default -> System.out.println("Type inconnu"); } }
c. Guarded Patterns (when)¶
On peut ajouter des conditions supplémentaires avec when :
void checkObject(Object obj) {
switch (obj) {
case String s when s.length() > 5 -> System.out.println("Longue chaîne");
case String s -> System.out.println("Chaîne courte");
case Integer i when i > 100 -> System.out.println("Grand nombre");
case Integer i -> System.out.println("Petit nombre");
default -> System.out.println("Autre type");
}
}
5. Pattern Matching dans les boucles for (depuis Java 21)¶
On peut utiliser le pattern matching directement dans les boucles for pour filtrer et caster les éléments :
List<Object> objects = List.of("hello", 42, new Point(1, 2), "world");
for (Object obj : objects) {
if(obj instanceof String s && s.length() > 3) {
System.out.println("Long string: "+s);
} else if(obj instanceof Point p) {
System.out.println("Point: "+p);
}
}
6. Record Patterns (depuis Java 19+)¶
Les record et le pattern matching sont faits pour s’entendre. On peut déconstruire un record directement dans un
switch ou un instanceof :
a. Avec instanceof¶
record Person(String name, int age) {
}
void printPersonInfo(Object obj) {
if (obj instanceof Person(String name, int age)) {
System.out.println(name + " a " + age + " ans.");
}
}
b. Avec switch¶
void printPersonDetails(Object obj) {
switch (obj) {
case Person(var name, var age) when age >= 18 -> System.out.println(name + " est adulte.");
case Person(var name, var age) -> System.out.println(name + " est mineur.");
default -> System.out.println("Pas une personne.");
}
}
7. Exercices pratiques¶
Exercice 1 : Pattern Matching avec instanceof¶
Écrivez une méthode qui prend un Object et affiche :
- La longueur si c’est une
String. - La valeur si c’est un
Integer. - Un message par défaut sinon.
Solution attendue :
void printInfo(Object obj) {
if (obj instanceof String s) {
System.out.println("Longueur : " + s.length());
} else if (obj instanceof Integer i) {
System.out.println("Valeur : " + i);
} else {
System.out.println("Type non géré");
}
}
Exercice 2 : switch avec Pattern Matching¶
Écrivez un switch qui :
- Affiche l’aire si l’objet est un
Circle(record Circle(double radius)). - Affiche le périmètre si l’objet est un
Rectangle(record Rectangle(double length, double width)). - Affiche un message par défaut sinon.
Solution attendue :
void printShapeInfo(Shape shape) {
switch (shape) {
case Circle c -> System.out.println("Aire : " + Math.PI * c.radius() * c.radius());
case Rectangle r -> System.out.println("Périmètre : " + 2 * (r.length() + r.width()));
default -> System.out.println("Forme inconnue");
}
}
Exercice 3 : Record Patterns¶
Créez un record Book(String title, int year) et écrivez une méthode qui utilise le pattern matching pour afficher :
- “Livre récent” si l’année est >= 2000.
- “Livre ancien” sinon.
Solution attendue :
record Book(String title, int year) {
}
void printBookInfo(Object obj) {
if (obj instanceof Book(var title, var year) && year >= 2000) {
System.out.println(title + " (récent)");
} else if (obj instanceof Book(var title, var year)) {
System.out.println(title + " (ancien)");
}
}
Exercice 4 : switch avec when¶
Utilisez un switch avec when pour afficher :
- “Long” si la
Stringa plus de 10 caractères. - “Court” sinon.
- “Nombre pair” si l’
Integerest pair. - “Nombre impair” sinon.
Solution attendue :
void printDetails(Object obj) {
switch (obj) {
case String s when s.length() > 10 -> System.out.println("Long");
case String s -> System.out.println("Court");
case Integer i when i % 2 == 0 -> System.out.println("Nombre pair");
case Integer i -> System.out.println("Nombre impair");
default -> System.out.println("Type inconnu");
}
}
8. Pattern Matching vs Polymorphisme : Quand utiliser quoi ?¶
a. Principe de design OO : Privilégier le polymorphisme¶
Dans un bon design OO, on privilégie généralement le polymorphisme pour modéliser des comportements variables. Par exemple, pour calculer l’aire de différentes formes géométriques, il est préférable de définir une méthode abstraite dans une interface ou une classe de base, et de l’implémenter dans chaque sous-classe :
interface Shape {
double getArea(); // Méthode polymorphe
}
record Circle(double radius) implements Shape {
@Override
public double getArea() {
return Math.PI * radius * radius;
}
}
record Rectangle(double length, double width) implements Shape {
@Override
public double getArea() {
return length * width;
}
}
// Utilisation polymorphe
void printArea(Shape shape) {
System.out.println("Aire : " + shape.getArea());
}
Avantages du polymorphisme :
- Extensibilité : Ajouter une nouvelle forme ne nécessite pas de modifier le code qui utilise
Shape. - Respect du principe Open/Closed : Le code est ouvert à l’extension mais fermé à la modification.
- Lisibilité : Le comportement est encapsulé dans chaque classe, ce qui rend le code plus clair et maintenable.
b. Quand le pattern matching est-il justifié ?¶
Le pattern matching peut être utile dans les cas suivants, où le polymorphisme n’est pas applicable ou peu pratique :
- Accès à des types externes ou non modifiables :
- Si vous utilisez des classes tierces (ex:
String,Integer,List) ou desrecordque vous ne pouvez pas modifier pour y ajouter des méthodes polymorphes.
- Logique transversale ou temporaire :
- Pour des traitements ponctuels qui ne justifient pas une refactorisation en polymorphisme (ex: sérialisation, logging, validation).
- Traitement de types hétérogènes :
- Quand vous manipulez une collection d’objets de types différents sans hiérarchie commune (ex:
List<Object>contenant desString,Integer, etc.).
- **Déconstruction de
record**:
- Pour extraire et utiliser directement les composants d’un
recordsans avoir besoin de méthodes intermédiaires.
- Cas où le polymorphisme est trop lourd :
- Pour des petits projets, des scripts, ou des prototypes où la surcharge du polymorphisme n’est pas justifiée.
c. Exemple : Quand le pattern matching est préférable¶
Supposons que vous deviez écrire une méthode qui affiche un message différent selon le type d’un objet que vous ne
contrôlez pas (ex: String, Integer, List) :
void printTypeInfo(Object obj) {
switch (obj) {
case String s -> System.out.println("Chaîne de longueur " + s.length());
case Integer i -> System.out.println("Nombre entier : " + i);
case List<?> list -> System.out.println("Liste de taille " + list.size());
default -> System.out.println("Type inconnu");
}
}
- Ici, le polymorphisme n’est pas applicable car vous ne pouvez pas modifier
String,Integer, ouList.
d. Exemple : Quand le polymorphisme est préférable¶
Reprenons l’exemple des formes géométriques. Utiliser le pattern matching pour calculer l’aire n’est pas recommandé si vous contrôlez les classes, car cela viole le principe de responsabilité unique et rend le code moins extensible :
// À éviter si vous pouvez utiliser le polymorphisme
void printArea(Shape shape) {
switch (shape) {
case Circle c -> System.out.println("Aire : " + Math.PI * c.radius() * c.radius());
case Rectangle r -> System.out.println("Aire : " + r.length() * r.width());
// Si vous ajoutez un Triangle, il faut modifier cette méthode !
}
}
9. Limites du polymorphisme¶
Il existe des cas où le polymorphisme n’est pas applicable ou peu pratique :
- Types finaux ou externes :
- Impossible d’étendre ou de modifier des classes comme
String,Integer, ou des classes tierces.
- Logique dépendante du type concret :
- Certaines opérations (ex: sérialisation, clonage, comparaison) dépendent du type concret et ne peuvent pas être encapsulées dans une hiérarchie polymorphe.
- Hiérarchies complexes ou absentes :
- Si les objets proviennent de sources différentes sans hiérarchie commune, le polymorphisme n’est pas utilisable.
- Performances :
- Dans certains cas (ex: traitement de données en masse), le pattern matching peut être plus performant que des appels polymorphes (bien que cela soit rare en Java moderne).
- Code temporaire ou prototype :
- Pour des scripts ou des prototypes, le pattern matching peut être plus rapide à écrire.
10. Bonnes pratiques : Comment choisir ?¶
| Critère | Polymorphisme | Pattern Matching |
|---|---|---|
| Vous contrôlez les classes | ✅ Recommandé | ❌ À éviter |
| Vous ne contrôlez pas les classes | ❌ Impossible | ✅ Justifié |
| Logique extensible | ✅ Idéal | ❌ Risque de duplication |
| Logique ponctuelle ou transversale | ❌ Peu adapté | ✅ Adapté |
| Respect des principes SOLID | ✅ Oui | ⚠️ Risque de violation (OCP) |
| Lisibilité et maintenabilité | ✅ Meilleure | ⚠️ Peut devenir complexe |
11. Exemple complet : Combiner les deux approches¶
Parfois, une solution hybride est optimale. Par exemple, utilisez le polymorphisme pour les comportements principaux, et le pattern matching pour des traitements spécifiques :
interface Shape {
double getArea(); // Polymorphisme pour le comportement principal
}
record Circle(double radius) implements Shape {
@Override
public double getArea() {
return Math.PI * radius * radius;
}
}
record Rectangle(double length, double width) implements Shape {
@Override
public double getArea() {
return length * width;
}
}
// Utilisation polymorphe pour l'aire
void printArea(Shape shape) {
System.out.println("Aire : " + shape.getArea());
}
// Utilisation de pattern matching pour un traitement spécifique
void printShapeInfo(Object obj) {
switch (obj) {
case Circle c -> System.out.println("Cercle de rayon " + c.radius());
case Rectangle r -> System.out.println("Rectangle " + r.length() + "x" + r.width());
case Shape s -> System.out.println("Forme inconnue avec aire : " + s.getArea());
default -> System.out.println("Pas une forme");
}
}
12. Conclusion¶
- Privilégiez le polymorphisme pour modéliser des comportements variables dans une hiérarchie de classes que vous contrôlez.
- Utilisez le pattern matching pour des traitements ponctuels, des types externes, ou des cas où le polymorphisme n’est pas applicable.
- Évitez le pattern matching pour des logiques métiers centrales qui pourraient évoluer, afin de respecter les principes SOLID et de maintenir un code extensible.
En combinant judicieusement ces deux approches, vous pouvez écrire du code Java moderne, expressif et maintenable.
13. Exercice récapitulatif¶
Exercice : Choisir la bonne approche¶
Pour chacun des cas suivants, indiquez si vous utiliseriez le polymorphisme ou le pattern matching, et justifiez votre choix :
- Calculer le périmètre de différentes formes géométriques (
Circle,Rectangle,Triangle) que vous contrôlez. - Afficher un message différent selon qu’un objet est une
String, unInteger, ou uneList(types JDK). - Valider qu’un objet est soit un
Usersoit unAdmin(classes que vous contrôlez), et extraire leur nom. - Écrire un logger qui affiche des informations différentes selon le type d’une exception (
IOException,SQLException, etc.).
Solutions suggérées
- Polymorphisme : Ajoutez une méthode
getPerimeter()dans l’interfaceShape. - Pattern Matching : Impossible d’utiliser le polymorphisme sur des types JDK.
- Polymorphisme : Ajoutez une méthode
getName()dans une interface communePerson. - Pattern Matching : Les exceptions sont des classes externes, et la logique est transversale (logging).