Guide complet pour changer d'expressions dans Java 12


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()); // as we'll see in "Exhaustiveness", `default` is not necessary default -> throw new IllegalArgumentException("Seriously?!"); }; 

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; // don't forget to `break` or you're screwed! break; case FALSE: result = false; break; case FILE_NOT_FOUND: // intermediate variable for demo purposes; // wait for it... var ex = new UncheckedIOException("This is ridiculous!", new FileNotFoundException()); throw ex; default: // ... here we go: // can't declare another variable with the same name var ex2 = new IllegalArgumentException("Seriously?!"); throw ex2; } 

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()); // without default branch, the method wouldn't compile default: throw new IllegalArgumentException("Seriously?!"); } } 

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()); // as we'll see in "Exhaustiveness", `default` is not necessary default -> throw new IllegalArgumentException("Seriously?!"); }; 

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"; // `default, case FILE_NOT_FOUND -> ...` does not work // (neither does other way around), but that makes // sense because using only `default` suffices default -> "insane"; }; 

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"); // in colon-form, if `ternaryBool` is `TRUE` or `FALSE`, // we would see both messages; in arrow-form, only one // branch is executed default -> System.out.println("Bool was insane"); } 

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"); // return with `break`, not `return` break true; } case FALSE -> { System.out.println("Bool false"); break false; } case FILE_NOT_FOUND -> { var ex = new UncheckedIOException("This is ridiculous!", new FileNotFoundException()); throw ex; } default -> { var ex = new IllegalArgumentException("Seriously?!"); throw ex; } }; 

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"; // note that we don't throw the exception! // but it's `Serializable`, so it matches the target type default -> new IllegalArgumentException("insane"); }; 

Que va-t-il se passer maintenant?


 // compiler infers super type of `String` and // `IllegalArgumentException` ~> `Serializable` var serializableMessage = switch (bool) { case TRUE, FALSE -> "sane"; // note that we don't throw the exception! default -> new IllegalArgumentException("insane"); }; 

(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) { // `return` is only possible from block case TRUE, FALSE -> { return "sane"; } default -> { return "This is ridiculous!"; } }; } 

... vous ne pouvez pas utiliser return dans une expression ...


 public String sanity(Bool ternaryBool) { String result = switch (ternaryBool) { // this does not compile - error: // "return outside of enclosing switch expression" case TRUE, FALSE -> { return "sane"; } default -> { return "This is ridiculous!"; } }; } 

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:


 // compile error: // "the switch expression does not cover all possible input values" boolean result = switch (ternaryBool) { case TRUE -> true; // no case for `FALSE` case FILE_NOT_FOUND -> throw new UncheckedIOException( "This is ridiculous!", new FileNotFoundException()); }; 

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 .


 // compiles without `default` branch because // all cases for `ternaryBool` are covered boolean result = switch (ternaryBool) { case TRUE -> true; case FALSE -> false; case FILE_NOT_FOUND -> throw new UncheckedIOException( "This is ridiculous!", new FileNotFoundException()); }; 

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?

Source: https://habr.com/ru/post/fr443464/


All Articles