Le bon vieux switch
en Java depuis le premier jour. Nous l'utilisons tous et nous y sommes habitués - en particulier ses bizarreries. (Est-ce que quelqu'un d'autre est ennuyé par la break
?) Mais maintenant tout commence à changer: dans Java 12, le commutateur au lieu d'un opérateur est devenu une expression:
boolean result = switch(ternaryBool) { case TRUE -> true; case FALSE -> false; case FILE_NOT_FOUND -> throw new UncheckedIOException( "This is ridiculous!", new FileNotFoundException());
Switch a désormais la possibilité de renvoyer le résultat de son travail, qui peut être affecté à une variable; Vous pouvez également utiliser la syntaxe de style lambda, qui vous permet de vous débarrasser de la transmission pour tous les case
où il n'y a pas d'instruction break
.
Dans ce guide, je vais vous expliquer tout ce que vous devez savoir sur les expressions de commutateur dans Java 12.
Aperçu
Selon les spécifications préliminaires du langage , les expressions de commutateur commencent tout juste à être implémentées dans Java 12.
Cela signifie que cette construction de contrôle peut être modifiée dans les futures versions de la spécification de langage.
Pour commencer à utiliser la nouvelle version de switch
vous devez utiliser l'option de ligne de commande --enable-preview
pendant la compilation et au démarrage du programme (vous devez également utiliser --release 12
lors de la compilation - note du traducteur).
Gardez donc à l'esprit que le commutateur , en tant qu'expression, n'a pas actuellement la syntaxe finale dans Java 12.
Si vous avez envie de jouer avec tout cela vous-même, vous pouvez visiter mon projet de démonstration Java X sur un github .
Problème avec les instructions dans le commutateur
Avant de passer à un aperçu des innovations en matière de commutation , évaluons rapidement une situation. Supposons que nous soyons confrontés à une "terrible" boulée ternaire et que nous voulions la convertir en boulée ordinaire. Voici une façon de procéder:
boolean result; switch(ternaryBool) { case TRUE: result = true;
D'accord, c'est très gênant. Comme de nombreuses autres options de commutation trouvées dans "nature", l'exemple ci-dessus calcule simplement la valeur d'une variable et l'affecte, mais l'implémentation est contournée (déclarer le result
l'identifiant et l'utiliser plus tard), répétée (ma break
'et toujours le résultat de copier-coller) et sujette aux erreurs (vous avez oublié une autre branche? Oh!). Il y a clairement quelque chose à améliorer.
Essayons de résoudre ces problèmes en plaçant le commutateur dans une méthode distincte:
private static boolean toBoolean(Bool ternaryBool) { switch(ternaryBool) { case TRUE: return true; case FALSE: return false; case FILE_NOT_FOUND: throw new UncheckedIOException("This is ridiculous!", new FileNotFoundException());
C'est beaucoup mieux: il n'y a pas de variable fictive, pas de break
encombrant le code et les messages du compilateur sur l'absence de default
(même si ce n'est pas nécessaire, comme dans ce cas).
Mais, si vous y réfléchissez, nous ne sommes pas tenus de créer des méthodes juste pour contourner la fonction de langage maladroite. Et cela sans même considérer qu'un tel refactoring n'est pas toujours possible. Non, nous avons besoin d'une meilleure solution!
Présentation des expressions de commutateur!
Comme je l'ai montré au début de l'article, à partir de Java 12 et supérieur, vous pouvez résoudre le problème ci-dessus comme suit:
boolean result = switch(ternaryBool) { case TRUE -> true; case FALSE -> false; case FILE_NOT_FOUND -> throw new UncheckedIOException( "This is ridiculous!", new FileNotFoundException());
Je pense que c'est assez évident: si ternartBool
est TRUE
, alors le result
'sera défini sur true
(en d'autres termes, TRUE
devient true
). FALSE
devient false
.
Deux pensées surgissent immédiatement:
switch
peut avoir un résultat;- qu'est-ce que les flèches?
Avant de me plonger dans les détails des nouvelles fonctionnalités du commutateur , je parlerai au début de ces deux aspects principaux.
Expression ou déclaration
Vous serez peut-être surpris que le changement soit désormais une expression. Mais qu'était-il avant?
Avant Java 12, un commutateur était un opérateur - une construction impérative qui régule le flux de contrôle.
Considérez les différences entre les anciennes et les nouvelles versions de switch comme la différence entre if
et l'opérateur ternaire. Ils vérifient tous les deux la condition logique et effectuent des branchements en fonction de son résultat.
La différence est que if
exécute simplement le bloc correspondant, tandis que l'opérateur ternaire renvoie un résultat:
if(condition) { result = doThis(); } else { result = doThat(); } result = condition ? doThis() : doThat();
Il en va de même pour switch : avant Java 12, si vous vouliez calculer la valeur et enregistrer le résultat, vous devez soit l'assigner à une variable (puis la break
), soit la renvoyer à partir d'une méthode créée spécifiquement pour l' switch
.
Maintenant, l'expression entière de l' instruction switch est évaluée (la branche correspondante est sélectionnée pour exécution) et le résultat des calculs peut être affecté à une variable.
Une autre différence entre l'expression et l'instruction est que l' instruction switch , car elle fait partie de l'instruction, doit se terminer par un point-virgule, contrairement à l' instruction switch classique.
Flèche ou deux points
L'exemple d'introduction a utilisé la nouvelle syntaxe de style lambda avec une flèche entre l'étiquette et la partie en cours d'exécution. Il est important de comprendre que pour cela, il n'est pas nécessaire d'utiliser switch
comme expression. En fait, l'exemple ci-dessous est équivalent au code donné au début de l'article:
boolean result = switch(ternaryBool) { case TRUE: break true; case FALSE: break false; case FILE_NOT_FOUND: throw new UncheckedIOException( "This is ridiculous!", new FileNotFoundException()); default: throw new IllegalArgumentException("Seriously?!!?"); };
Notez que vous pouvez maintenant utiliser break
avec une valeur! Cela correspond parfaitement aux switch
à l'ancienne qui utilisent break
sans aucune signification. Alors, dans quel cas une flèche signifie-t-elle une expression au lieu d'un opérateur, pourquoi est-elle ici? Juste une syntaxe hipster?
Historiquement, les deux-points marquent simplement le point d'entrée du bloc d'instructions. À partir de ce moment, l'exécution de tout le code ci-dessous commence, même lorsqu'une autre étiquette est rencontrée. En switch
nous savons que cela passe au case
suivant (chute): l'étiquette du case
détermine où le flux de contrôle passe. Pour le compléter, vous avez besoin d'une break
ou d'un return
.
À son tour, l'utilisation de la flèche signifie que seul le bloc à sa droite sera exécuté. Et aucun "échec".
En savoir plus sur l'évolution du commutateur
Plusieurs étiquettes sur le boîtier
Jusqu'à présent, chaque case
qu'une seule étiquette. Mais maintenant, tout a changé - un case
peut correspondre à plusieurs étiquettes:
String result = switch(ternaryBool) { case TRUE, FALSE -> "sane";
Le comportement doit être évident: TRUE
et FALSE
produisent le même résultat - l'expression "sain" est évaluée.
Il s'agit d'une innovation plutôt intéressante qui a remplacé l'utilisation multiple du case
lorsqu'il était nécessaire de mettre en œuvre une transition directe vers le case
suivant.
Types en dehors d'Enum
Tous les exemples de switch
de cet article utilisent enum
. Et les autres types? Les expressions et les switch
peuvent également fonctionner avec String
, int
, (consultez la documentation ) short
, byte
, char
et leurs wrappers. Jusqu'à présent, rien n'a changé, bien que l'idée d'utiliser des types de données tels que float
et long
soit toujours valide (du deuxième au dernier paragraphe).
Plus sur la flèche
Examinons deux propriétés spécifiques à la forme de flèche d'un enregistrement séparateur:
- absence de transition de bout en bout au
case
suivant; - blocs d'opérateurs.
Pas de transmission au cas suivant
Voici ce que JEP 325 dit à ce sujet:
La conception actuelle de l' switch
en Java est étroitement liée aux langages tels que C et C ++ et prend en charge la sémantique de bout en bout par défaut. Bien que cette méthode traditionnelle de contrôle soit souvent utile pour écrire du code de bas niveau (comme les analyseurs pour le codage binaire), puisque le switch
utilisé dans le code de niveau supérieur, les erreurs de cette approche commencent à l'emporter sur sa flexibilité.
Je suis totalement d'accord et salue l'opportunité d'utiliser switch sans comportement par défaut:
switch(ternaryBool) { case TRUE, FALSE -> System.out.println("Bool was sane");
Il est important d'apprendre que cela n'a rien à voir avec l'utilisation de switch comme expression ou comme instruction. Le facteur décisif ici est la flèche contre le côlon.
Blocs opérateur
Comme dans le cas des lambdas, la flèche peut pointer vers un seul opérateur (comme ci-dessus) ou un bloc mis en évidence avec des accolades:
boolean result = switch(Bool.random()) { case TRUE -> { System.out.println("Bool true");
Les blocs qui doivent être créés pour les opérateurs multilignes ont un avantage supplémentaire (qui n'est pas requis lors de l'utilisation de deux points), ce qui signifie que pour utiliser les mêmes noms de variables dans différentes branches, le switch
ne nécessite pas de traitement spécial.
S'il vous a semblé inhabituel de sortir des blocs en utilisant la break
plutôt que le return
, ne vous inquiétez pas - cela m'a également intrigué et m'a semblé étrange. Mais ensuite, j'y ai réfléchi et je suis arrivé à la conclusion que cela avait du sens, car il préserve l'ancien style de la construction de switch
, qui utilise la break
sans valeurs.
En savoir plus sur les instructions switch
Et enfin et surtout, les spécificités de l'utilisation de switch
comme expression:
- expressions multiples;
- retour anticipé (
return
anticipé); - couverture de toutes les valeurs.
Veuillez noter que peu importe le formulaire utilisé!
Expressions multiples
Les expressions de commutateur sont des expressions multiples. Cela signifie qu'ils n'ont pas leur propre type, mais peuvent être parmi plusieurs types. Le plus souvent, les expressions lambda sont utilisées comme telles: s -> s + " "
, peuvent être Function<String, String>
, mais peuvent également être Function<Serializable, Object>
ou UnaryOperator<String>
.
À l'aide d'expressions de commutateur, un type est déterminé par l'interaction entre l'endroit où le commutateur est utilisé et les types de ses branches. Si une expression de commutateur est affectée à une variable typée, passée en argument ou autrement utilisée dans un contexte où le type exact est connu (c'est ce qu'on appelle le type cible), toutes ses branches doivent correspondre à ce type. Voici ce que nous avons fait jusqu'à présent:
String result = switch (ternaryBool) { case TRUE, FALSE -> "sane"; default -> "insane"; };
Par conséquent, le switch
affecté à la variable de result
de type String
. Par conséquent, String
est le type cible et toutes les branches doivent renvoyer un résultat de type String
.
La même chose se produit ici:
Serializable serializableMessage = switch (bool) { case TRUE, FALSE -> "sane";
Que va-t-il se passer maintenant?
(Pour l'utilisation du type var, lisez dans notre dernier article 26 recommandations pour l'utilisation du type var en Java - note du traducteur)
Si le type cible est inconnu, du fait que nous utilisons var, le type est calculé en trouvant le supertype le plus spécifique des types créés par les branches.
Retour anticipé
La conséquence de la différence entre l'expression et l' switch
est que vous pouvez utiliser return
pour quitter l' switch
:
public String sanity(Bool ternaryBool) { switch (ternaryBool) {
... vous ne pouvez pas utiliser return
dans une expression ...
public String sanity(Bool ternaryBool) { String result = switch (ternaryBool) {
Cela a du sens si vous utilisez une flèche ou un deux-points.
Couvrant toutes les options
Si vous utilisez switch
comme opérateur, peu importe que toutes les options soient couvertes ou non. Bien sûr, vous pouvez accidentellement ignorer le case
, et le code ne fonctionnera pas correctement, mais le compilateur ne s'en soucie pas - vous, votre IDE et vos outils d'analyse de code resteront seuls.
Les expressions de commutateur aggravent ce problème. Où doit-on passer si l'étiquette souhaitée est manquante? La seule réponse que Java puisse donner est de retourner null
pour les types de référence et une valeur par défaut pour les primitives. Cela provoquerait beaucoup d'erreurs dans le code principal.
Pour éviter un tel résultat, le compilateur peut vous aider. Pour les instructions switch, le compilateur insistera pour que toutes les options possibles soient couvertes. Regardons un exemple qui pourrait conduire à une erreur de compilation:
La solution suivante est intéressante: l'ajout de la branche default
corrigera certainement l'erreur, mais ce n'est pas la seule solution - vous pouvez toujours ajouter un case
pour FALSE
.
Oui, le compilateur pourra enfin déterminer si toutes les valeurs d'énumération sont couvertes (si toutes les options sont épuisées), et ne pas définir de valeurs par défaut inutiles! Asseyons-nous un instant dans une gratitude silencieuse.
Bien que cela soulève encore une question. Et si quelqu'un prend et transforme un booléen fou en booléen quaternion en ajoutant une quatrième valeur? Si vous recompilez l'expression de commutateur pour le Bool étendu, vous obtiendrez une erreur de compilation (l'expression n'est plus exhaustive). Sans recompilation, cela se transformera en un problème d'exécution. Pour résoudre ce problème, le compilateur passe à la branche default
, qui se comporte de la même manière que celle que nous avons utilisée jusqu'à présent, lançant une exception.
Dans Java 12, l'étendue de toutes les valeurs sans la branche default
ne fonctionne que pour l' enum
, mais lorsque le switch
devient plus puissant dans les futures versions de Java, il peut également fonctionner avec des types arbitraires. Si les étiquettes de case
peuvent non seulement vérifier l'égalité, mais aussi faire des comparaisons (par exemple, _ <5 -> ...) - cela couvrira toutes les options pour les types numériques.
Penser
Nous avons appris de l'article que Java 12 transforme un switch
en une expression, lui donnant de nouvelles fonctionnalités:
- maintenant un
case
peut correspondre à plusieurs étiquettes; - Le nouveau
case … -> …
forme de flèche case … -> …
suit la syntaxe des expressions lambda:
- les opérateurs ou blocs sur une seule ligne sont autorisés;
- le passage au
case
suivant case
empêché;
- maintenant, l'expression entière est évaluée comme une valeur, qui peut ensuite être affectée à une variable ou passée dans le cadre d'une instruction plus grande;
- expression multiple: si le type cible est connu, alors toutes les branches doivent lui correspondre. Sinon, un type spécifique est défini qui correspond à toutes les branches;
break
peut renvoyer une valeur d'un bloc;- pour une expression de
switch
utilisant enum
, le compilateur vérifie la portée de toutes ses valeurs. Si default
absent, une branche est ajoutée qui lève une exception.
Où cela nous mènera-t-il? Tout d'abord, comme il ne s'agit pas de la version finale de switch
, vous avez toujours le temps de laisser des commentaires sur la liste de diffusion Amber si vous n'êtes pas d'accord avec quelque chose.
Ensuite, en supposant que le commutateur reste tel qu'il est actuellement, je pense que la forme de flèche deviendra la nouvelle option par défaut. Sans passage direct au case
suivant et avec des expressions lambda concises (il est très naturel d'avoir un cas et une instruction sur une ligne), le switch
semble beaucoup plus compact et n'affecte pas la lisibilité du code. Je suis sûr que je n'utiliserai deux points que si je dois traverser le passage.
Qu'en penses-tu? Satisfait de la tournure des événements?