JEP 441, Pattern Matching for switch
Contexte
Le principe est comme le filtrage par motif pour instanceof
mais en l’appliquant à l’instruction switch
.
Pour plus d’informations sur le filtrage par motif pour instanceof
, je vous conseille cette article JEP 394 - Pattern Matching for instanceof
Cette fonctionnalité a connu trois aperçu
Maintenant, c’est bien une fonctionnalité standard qui sera inclus dans le JDK 21.
Principe
Reprenons, le principe. Avant cette fonctionnalité, nous avons le code suivant :
static String formatter(Object o) {
String formatted = "unknown";
if (o instanceof Integer i) {
formatted = String.format("int %d", i);
} else if (o instanceof Long l) {
formatted = String.format("long %d", l);
} else if (o instanceof Double d) {
formatted = String.format("double %f", d);
} else if (o instanceof String s) {
formatted = String.format("String %s", s);
}
return formatted;
}
Nous allons pouvoir simplifier l’écriture du code en utilisant un filtrage par motif au niveau du switch
(et même une expression switch
).
static String formatterPatternSwitch(Object o) {
return switch (o) {
case Integer i -> String.format("int %d", i);
case Long l -> String.format("long %d", l);
case Double d -> String.format("double %f", d);
case String s -> String.format("String %s", s);
default -> o.toString();
};
}
Dominance des étiquettes de cas
Il se peut que plusieurs étiquettes soient possibles. Il a été défini que c’est l’ordre apparition qui serait pris en compte. Cela fonctionne comme pour les exceptions.
Prenons un exemple avec les classes suivantes : Forme
, Rectangle
et Triangle
:
class Forme {}
class Rectangle extends Forme {}
class Triangle extends Forme { int calculerAire() { ... } }
Nous pouvons écrire le code suivant en respectez bien l’ordre.
static void first(Object obj) {
switch (obj) {
case Triangle t ->
System.out.println("C'est un triangle : " + t.calculerAire());
case Forme f ->
System.out.println("C'est une forme");
default -> {
break;
}
}
}
Sinon, cela ne compile pas comme le code suivant :
static void first(Object obj) {
switch (obj) {
case Forme f ->
System.out.println("C'est une forme");
case Triangle t -> (1)
System.out.println("C'est un triangle : " + c.calculerAire());
default -> {
break;
}
}
}
-
ERREUR, un motif est dominé par un motif précédent (Triangle est une Forme)
Dans les exemples, nous manipulatons beaucoup les types mais les étiquettes constantes fonctionnent aussi bien. Voici un exemple:
Integer i = ...
switch (i) {
case -1, 1 -> ... (1)
case Integer j -> ... (2)
}
-
Cas spécifique
-
Tous les autres cas
Raffinement des cas
Au delà du motif avec le type correspondant, nous pourrions avoir des conditions supplémentaires qui permet de raffiner le cas.
Nous pouvons traiter par type, entre Rectangle
et Triangle
. Cependant, nous aimerions définir des conditions supplémentaires. Par exemple, nous voulons traiter les triangles dont l’aire est supérieur à 100.
Voici le code que nous devons écrire :
static void testTriangle(Forme f) {
switch (f) {
case null:
break;
case Triangle t:
if (t.calculerAire() > 100) {
System.out.println("Grand triangle");
break;
}
default:
System.out.println("Une forme, qui peut être un petit triangle");
}
}
Pour cela, un nouveau mot-clé when
existe. Il permet de raffiner le cas. Dans notre exemple, nous avons t.calculateArea() > 100
static void testTriangle(Forme f) {
switch (f) {
case Triangle t
when t.calculerAire() > 100 ->
System.out.println("Grand triangle");
default ->
System.out.println("Une forme, qui peut être un petit triangle");
}
}
Là encore, il est possible de mixer les composants :
static void testTriangle(Forme f) {
switch (f) {
case Triangle t
when t.calculerAire() > 100 ->
System.out.println("Grand triangle");
case Triangle t ->
// Soit les triangles inférieur ou égale à 100
System.out.println("Petit triangle");
default ->
System.out.println("Forme autre que triangle");
}
}
Gestion des enum
comme étiquette des cas
Prenons le code suivant qui défini une énumération PILE et FACE
sealed interface Monnaie permits Piece {}
enum Piece implements Monnaie { PILE, FACE }
Nous pouvons écrire la fonction suivante :
static void bonEnumSwitch1(Monnaie m) {
switch (m) {
case Piece.FACE -> { (1)
System.out.println("Face");
}
case Piece.PILE -> {
System.out.println("Pile");
}
}
}
-
Nom qualifié de la constante est utilisé comme étiquette
static void bonEnumSwitch2(Piece p) {
switch (p) {
case FACE -> {
System.out.println("Face");
}
case Piece.PILE -> { (1)
System.out.println("Pile");
}
}
}
-
Nom qualifié n’est pas nécessaire mais cela reste autorisé.
En revanche, le code suivant ne compile pas
static void mauvaisEnumSwitch(Monnaie m) {
switch (m) {
case Piece.FACE -> {
System.out.println("Face");
}
case PILE -> { (1)
System.out.println("Pile");
}
default -> {
System.out.println("Some currency");
}
}
}
-
ERREUR. Le nom qualifié doit être utilisé pour PILE car le type est
Monnaie
et nonPiece
.
Gestion du null
Jusqu’à présent, la déclaration où l’expression switch
avec une valeur null
lève une exception NullPointerException
. C’est pourquoi, généralement, le switch
est protégé par l’instruction if
.
static void testFooBar(String s) {
if (s == null) {
System.out.println("Oops!");
return;
}
switch (s) {
case "Foo", "Bar" -> System.out.println("Great");
default -> System.out.println("Ok");
}
}
Maintenant, nous pourrons écrire un label null
pour traiter ce cas. Ainsi, nous pouvons écrire :
static void testFooBar(String s) {
switch (s) {
case null -> System.out.println("Oops");
case "Foo", "Bar" -> System.out.println("Great");
default -> System.out.println("Ok");
}
}
Le label peut être mélangé avec un autre. Ainsi, nous pouvons écrire par exemple :
static void testStringOrNull(Object o) {
switch (o) {
case null, String s -> System.out.println("String: " + s);
default -> System.out.println("Something else");
}
}
Pour de raisons de compatibilité, si la valeur du sélecteur est nulle
, le switch
lève une exception NullPointerException
.
C’est à dire que le code suivant :
static void test(Object o) {
switch (o) {
case String s -> System.out.println("String: " + s);
case Integer i -> System.out.println("Integer");
default -> System.out.println("default");
}
}
est équivalent à celui-ci
static void test(Object o) {
switch (o) {
case null -> throw new NullPointerException();
case String s -> System.out.println("String: "+s);
case Integer i -> System.out.println("Integer");
default -> System.out.println("default");
}
}
Là encore, c’est le compilateur qui fait le travail.
Cela signifie aussi que le label default
ne peut pas correspondre à la valeur nulle
.
En revanche, nous pouvons le traiter comme tel si nous le souhaitons
static void test(Object o) {
switch (o) {
case String s -> System.out.println("String: "+s);
case Integer i -> System.out.println("Integer");
case null, default -> System.out.println("default ou nulle");
}
}
Exhaustivité des expressions et des instructions
L’expression switch
requiert que toutes les valeurs possibles soient prises en compte dans un bloc du switch.
Par conséquent, le code suivant est erroné :
static int coverage(Object obj) {
return switch (obj) { (1)
case String s -> s.length();
};
}
-
Erreur car non exhaustif (String est seulement une possibilité pour un Object)
Nous poursuivons en complétons l’expression switch
static int coverage(Object obj) {
return switch (obj) { (1)
case String s -> s.length();
case Integer i -> i;
};
}
-
Erreur car toujours pas exhaustif (String et Integer ne couvrent pas tous les cas)
Pour couvrir tous les cas, nous allons utiliser l’étiquette default
.
static int coverage(Object obj) {
return switch (obj) {
case String s -> s.length();
case Integer i -> i;
default -> 0;
};
}
De ce fait, nous pouvons utiliser le compilateur pour nous aider.
Partons sur le code suivant avec la définition de l’énumération Couleur (Rouge et Bleu) et une expression switch
enum Couleur { ROUGE, BLEU }
int nombreLettres = switch (couleur) {
case ROUGE -> 5;
case BLEU -> 4;
}
Maintenant, si on ajoute une nouvelle Couleur. Nous avons une erreur de compilation pour les expressions switch
qui ne sont pas à jour
enum Couleur { ROUGE, BLEU, ORANGE }
int nombreLettres = switch (couleur) { (1)
case ROUGE -> 5;
case BLEU -> 4;
}
-
Erreur cas tous les cas ne sont pas couverts.
Donc, il sera nécessaire de corriger afin que la compilation fonctionne
enum Couleur { ROUGE, BLEU, ORANGE }
int nombreLettres = switch (couleur) { (1)
case ROUGE -> 5;
case BLEU -> 4;
case ORANGE -> 6;
}
Là encore, le compilateur est notre allié.
D’où l’intérêt d’éviter l’usage de l’étiquette default
car sinon nous perdons l’avantage de ces contrôles par le compilateur.
Ce principe fonctionne aussi avec les classes scellées. Le compilateur provoque des erreurs si nous modifions la hiérarchie.
Exhaustivité et compatibilité
Le principe d’exhaustivité s’appliquent sur l’expression switch
et l’instruction switch
. Cependant, pour des raisons de compatibilité, les instructions switch
existant dans le code continuent à compiler même si l’exhaustivité n’est pas respecté, à condition qu’aucune nouvelle fonctionnalité soit utilisée.
ealed interface S permits A, B, C {}
final class A implements S {}
final class B implements S {}
record C(int i) implements S {} (1)
static void switchStatementExhaustive(S s) {
switch (s) { (2)
case A a :
System.out.println("A");
break;
case C c :
System.out.println("C");
break;
};
}
-
Implicitement final car c’est un enregistrement.
-
Normalement il devrait avoir une erreur car le
switch
n’est pas exhaustive. Cependant, l’absence de la classe B est permise pour des raisons de compatibilité.
Portée des variables des motifs
La portée des variables est étendu dans trois cas.
-
Portée d’une déclaration de variable de motif, incluant la condition (mot réservé
when
).
Cela donne le code suivant :
static void testPortee1(Object obj) {
switch (obj) {
case Character c
when c.charValue() == 7:
System.out.println("Ding!");
break;
default:
break;
}
}
-
Portée d’une déclaration de variable de motif dans une étiquette incluant l’expression, le bloc ou l’instruction
throw
qui apparaît à droite de la flèche.
Voici l’illustration avec le code suivant :
static void testPortee2(Object obj) {
switch (obj) {
case Character c -> {
if (c.charValue() == 7) {
System.out.println("Ding!");
}
System.out.println("Character");
}
case Integer i ->
throw new IllegalStateException("Argument invalide: "
+ i.intValue());
default -> {
break;
}
}
}
-
Portée d’une déclaration de variable de motif incluant un groupe d’instructions avec étiquette. La déclaration d’une variable de motif est interdit
Nous avons le code valide
static void testPorteValide(Object obj) {
switch (obj) {
case Character c:
if (c.charValue() == 7) {
System.out.print("Ding ");
}
if (c.charValue() == 9) {
System.out.print("Tab ");
}
System.out.println("Character");
default:
System.out.println();
}
}
En revanche, voici le code invalide
tatic void testScopeError(Object obj) {
switch (obj) {
case Character c:
if (c.charValue() == 7) {
System.out.print("Ding ");
}
if (c.charValue() == 9) {
System.out.print("Tab ");
}
System.out.println("character");
case Integer i: (1)
System.out.println("An integer " + i);
default:
break;
}
}
-
Erreur pendant la compilation
Moteur de recherche
"Eduquer, ce n'est pas remplir des vases mais c'est d'allumer des feux." - Michel Montaigne
Billets récents
- Eclipse plante systématiquement sous Debian (et autres distribution Linux)
- JEP 463, Implicitly Declared Classes and Instance Main Methods (Second Preview)
- Debian - Montée de version de Debian 11 (Bullseye) à Debian 12 (Bookworm)
- JEP 451, Prepare to Disallow the Dynamic Loading of Agents
- JEP 444, Virtual Threads