Un bref historique des versions récentes de Java.
Java a évolué rapidement au cours des dix dernières années. Java 9 a été la dernière version "lente", toutes les versions suivantes ayant été publiées à 6 mois d'intervalle. Voici un tableau montrant les mises à jour de Java au cours de la dernière décennie et les principaux changements syntaxiques/ajouts effectués dans chaque version (la plupart des changements sont omis pour rester dans le sujet).
Il y a quelques nouveautés notables.
Java 14 a stabilisé les expressions switch, 16 les enregistrements et la correspondance des motifs instanceof, 17 les classes scellées, et maintenant, 21 stabilisera les motifs d'enregistrement et la correspondance des motifs switch.
Cet ensemble de changements permet à Java d'exprimer l'un des fondements de la programmation fonctionnelle, ce que le langage n'avait jamais pu faire auparavant : les types de données algébriques, ainsi que les manières idiomatiques de les utiliser.
Les types de données algébriques sont un concept issu de la théorie des types, une branche de la théorie des ensembles qui se concentre spécifiquement sur des questions telles que "Une pomme est-elle un fruit ?" et d'autres énigmes fantaisistes que les professeurs de mathématiques aiment poser aux étudiants infortunés du monde entier.
Introduction minimale à certains termes de la théorie des types
La théorie des types est assez complexe, et la plupart d'entre elle n'est pas pertinente pour cet article.
Ainsi, au lieu d'expliquer la théorie des types, on va parler de quelques types spécifiques qu'il serait utile de connaître.
Bottom, ou type vide (⊥)
Ce type décrit l'ensemble des valeurs qui ne peuvent pas être calculées. Cet ensemble est généralement vide pour tout langage de programmation normal (Ø).
Aucun objet ne peut être coulé en bottom puisqu'il s'agit d'un ensemble vide.
Un exemple d'un tel type est le Nothing de Kotlin. L'existence d'une instance de Nothing est considérée comme une erreur. La façon dont Kotlin empêche l'instanciation de Nothing est de rendre son constructeur privé.
La version Java de ce type est Void, la classe enveloppante du type primitif Void. Là encore, il est impossible de construire une instance de Void car son constructeur est privé. Cependant, Void ne peut pas être considéré comme un véritable type inférieur car une variable Void peut toujours contenir null, ce qui signifie qu'il s'agit techniquement d'un type unitaire. En ce sens, la primitive Void est plus performante. Il n'y a absolument aucun moyen de l'utiliser comme type de variable, donc vous ne pouvez même pas avoir une variable void vide.
Top, ou type supérieur (⊤)
Ce type représente toutes les valeurs de tous les types - l'ensemble universel des valeurs, U. En Kotlin, ce type s'appelle Any. Il peut être tentant de penser qu'Object (l'équivalent Java de Any) est un type supérieur, si ce n'est que les primitives existent. Les primitives sont totalement distinctes du modèle d'objet de Java et interagissent avec les objets de manière assez peu standard. Pour cette raison, Java n'a techniquement pas de type supérieur comme d'autres langages.
Pendant ce temps, C fait ce qu'il fait et surcharge void en utilisant void * pour représenter un pointeur qui peut faire référence à une valeur de n'importe quel type à la place. Comme c'est drôle !
Tous les objets peuvent être castés en top puisque U contient toutes les valeurs qui existent.
Il n'y a pas grand chose à dire sur top, si ce n'est qu'une variable de ce type peut contenir n'importe quoi. Y compris une valeur de type bottom. Bonne chance pour trouver cette valeur, cependant.
Unit, ou le type unité (())
Ce type n'a qu'une seule valeur. Il n'existe qu'une seule instance de cette valeur, et il est impossible d'en créer d'autres.
La primitive void de Java fonctionne techniquement comme suit. Lorsqu'une méthode renvoie void, vous pouvez la traiter comme si elle renvoyait implicitement la seule instance du type void sous le capot (ce n'est pas ainsi que la JVM gère void). Java s'écarte de la norme théorique en ce sens que void ne peut jamais être transmis à une méthode en tant que paramètre.
Remarque
En réalité, Java ne fait que fusionner les types bottom et unit pour nous donner void.
En réalité, Java ne fait que fusionner les types bottom et unit pour nous donner void.
En fait, c'est précisément ainsi que Kotlin définit sa propre Unit. Si vous naviguez jusqu'à la définition de Unit, vous verrez à quel point elle est simple : il s'agit simplement d'un object !
Contrairement à Java, Kotlin permet d'utiliser Unit n'importe où, y compris en tant que paramètre d'une méthode. L'extrait suivant est donc légal :
Code : | Sélectionner tout |
1 2 3 | fun identity(param1: Unit): Unit = param1 val result = identity(param1 = Unit) // just returns the Unit instance again. |
Le type booléen possède deux valeurs valides, true et false (ou tout autre nom que vous souhaitez utiliser). En fait, vous n'avez même pas besoin d'utiliser le type booléen natif de votre langage pour représenter ce type ; vous pouvez tout aussi bien utiliser une instance nullable d'un type d'unité. Si la variable est non nulle, elle est true, et si elle est null, elle est false. Il s'agit bien sûr d'une perte de temps inutile qui ne convient qu'à ceux qui souhaitent obscurcir leur code source.
Jusqu'à présent, nous avons examiné quelques exemples de base de "règles" utilisées pour définir les types dans la théorie des types. Entrons dans le vif du sujet et discutons des types somme et produit, et de la manière dont Java 21 nous permet de les représenter par le biais d'enregistrements et de classes scellées.
Produit de types
Les Produit de types sont composés de deux ou plusieurs types constitutifs. En général, un Produit de types est une liste de deux types ou plus regroupés. L'arité, ou le degré, d'un Produit de types est le nombre de types qui le composent.
Si vous souhaitez un exemple concret et agréable de Produit de types, ne cherchez pas plus loin que l'humble C struct:
Code : | Sélectionner tout |
1 2 3 4 5 6 | struct some_type { int val1; // Type 1 char *val2; // Type 2 double val3; // Type 3 int val4; // Type 4 }; |
Dans le cas présent, nous disposons déjà des outils nécessaires. Nous associons chaque type au nom qui lui est donné dans la structure (Duh). D'un point de vue mathématique, un Produit de types n'est pas simplement une liste de types, mais une liste de paires ordonnées, où chaque paire ordonnée est constituée d'un type et d'un nom associé à ce type.
Par exemple, nous pouvons représenter la première valeur d'un some_type sous la forme d'une paire ordonnée (int, "val1". De cette façon, il est impossible de confondre les deux composants int ; ils ont des noms différents !
Mais qu'en est-il des tuples comme en Python ou en Rust ?
Vous pouvez les considérer comme des produits de types où le "nom" est l'index du type de composant dans le tuple.
Code : | Sélectionner tout |
1 2 3 4 5 6 | some_tuple = (1, '2', True, 5) int_1 = some_tuple[0] # (int, 0) str_2 = some_tuple[1] # (str, 1) ... |
En théorie des ensembles, le mot produit fait généralement référence au produit cartésien de deux ensembles.
Remarque
Le produit cartésien de deux ensembles est un ensemble de paires ordonnées constitué de toutes les combinaisons possibles de chaque élément des deux ensembles.
En termes mathématiques simples, le nombre d'éléments dans le produit cartésien C de deux ensembles A et B est le produit du nombre d'éléments dans A et du nombre d'éléments dans B.
Le produit cartésien de deux ensembles est un ensemble de paires ordonnées constitué de toutes les combinaisons possibles de chaque élément des deux ensembles.
En termes mathématiques simples, le nombre d'éléments dans le produit cartésien C de deux ensembles A et B est le produit du nombre d'éléments dans A et du nombre d'éléments dans B.
Code : | Sélectionner tout |
some_type = int × char* × double × int
Code : | Sélectionner tout |
C = A × B = {(a, b) | a ∈ A, b ∈ B}
Code : | Sélectionner tout |
some_type = { (val1, val2, val3, val4) | val1 ∈ int, val2 ∈ char*, val3 ∈ double, val4 ∈ int }
Lorsque Java 16 a été publié, la fonction de classe d'enregistrement a été stabilisée. Les classes d'enregistrement sont un excellent exemple de ce qu'est un Produit de types. Tous les champs sont finaux ; on ne peut pas non plus hériter de ces classes. Tout l'état d'un enregistrement est défini au moment de sa construction et une fois qu'un enregistrement est créé, c'est à cela qu'il ressemblera pour le reste de sa durée de vie, soit 200 millisecondes.
Cela contraste avec les classes Java normales, qui sont très hétérogènes. Vous pouvez avoir un état public et un état privé, il y a un état caché via l'héritage auquel vous ne pensez pas jusqu'à ce qu'il apparaisse comme un animatronic hanté sous caféine pour vous effrayer avec des bugs bizarres, et vous pouvez avoir des champs mutables, des champs statiques, et toutes sortes d'autres choses distrayantes qui yadda yadda yadda... (vous avez compris l'idée).
Le problème avec les types Java normaux est qu'il est impossible de généraliser les composants d'un type. Et c'est important quand tout ce que vous voulez, c'est traiter efficacement des données ; vous devez naviguer dans un labyrinthe de getters potentiellement non standard pour accéder à vos données en premier lieu, sans parler de les manipuler.
Java n'a jamais supporté la déstructuration comme le font JavaScript ou Rust. Mais même si Java l'avait supportée, la spécification aurait probablement limité cette fonctionnalité aux enregistrements. Posons-nous quelques questions pour mieux comprendre pourquoi.
Remarque
La déstructuration est une fonctionnalité que certains langages (très célèbre, JavaScript) possèdent et qui vous permet de prendre une valeur complexe, et, pour emprunter un PHPisme, de l'EXPLOITER en ses composants comme une liste de variables complètement indépendantes.
La déstructuration est une fonctionnalité que certains langages (très célèbre, JavaScript) possèdent et qui vous permet de prendre une valeur complexe, et, pour emprunter un PHPisme, de l'EXPLOITER en ses composants comme une liste de variables complètement indépendantes.
Comment déstructurer une classe Java normale ?
L'état interne d'une classe Java comprend tous ses champs, qu'ils soient publics ou privés. Cependant, permettre l'extraction de champs privés par la déstructuration ne semble pas être une bonne idée ; nous savons tous à quel point le vieil oncle Bob s'énerve lorsqu'il s'agit de rompre l'encapsulation. Nous savons tous à quel point le vieil oncle Bob s'énerve lorsqu'il s'agit de briser l'encapsulation.
Qu'en est-il alors de l'état public ?
Réfléchissons d'abord à cette question : Comment les objets Java exposent-ils l'état public ? Bien sûr, vous pouvez définir un champ comme public, et si vous voulez empêcher toute modification indue, rendez le champ final. Mais il existe également une autre approche extrêmement courante. La plupart des objets Java définissent chaque champ comme privé et rendent tous les champs accessibles uniquement par le biais de méthodes d'accès pour la lecture et l'écriture.
Malheureusement, il n'y a pas de conventions imposées par le langage pour définir les accesseurs ; vous pourriez donner à l'accesseur de foo le nom de getBar, et cela fonctionnerait toujours bien, à l'exception du fait qu'il confondrait toute personne essayant d'accéder à bar et non à `foo'.
Bien sûr, vous pouvez utiliser des frameworks comme Lombok pour éliminer la complexité et l'incertitude en plaçant quelques annotations sur vos classes POJO, mais cela ne change pas le fait sous-jacent que les classes normales en Java sont très difficiles à raisonner statiquement en raison du nombre de "variables" qui contribuent à définir l'état d'une classe.
C'est peut-être l'un des problèmes qui a empêché les auteurs de la spécification du langage Java d'ajouter d'emblée le filtrage à toutes les classes.
Pour résoudre ce problème, ils ont créé les enregistrements, une hiérarchie de classes entièrement différente. Il existe déjà un précédent : Java 5 a introduit les enums, qui héritent de java.lang.Enum. De même, tous les enregistrements héritent de java.lang.Record.
Alors comment les enregistrements font-ils ce que les classes normales ne font pas ?
Les enregistrements résolvent ce problème en restreignant la manière dont ils peuvent être définis et en définissant de manière rigide l'ensemble des propriétés qu'ils peuvent avoir.
Plus précisément :
- Les enregistrements sont implicitement des classes finales et ne peuvent pas être hérités.
- Il n'y a plus de classes enfants illégitimes nées d'une aventure avec une bibliothèque concubine entièrement différente.
- Les enregistrements ne peuvent étendre aucune classe autre que java.lang.Record.
- Cela permet d'éviter que l'état hérité ne pollue le code de l'enregistrement.
- Les composants d'un enregistrement ne peuvent pas avoir de modificateurs de visibilité.
- Les composants d'un enregistrement sont toujours finaux et immuables.
- Toutefois, l'immuabilité ne s'étend pas au contenu de chaque composant de l'enregistrement.
- Seules les références des composants d'un enregistrement sont considérées comme immuables.
- Lorsque vous déclarez un enregistrement et que vous ne définissez pas de méthodes d'obtention, les méthodes d'obtention seront définies à l'aide d'une syntaxe très spécifique.
- Cette syntaxe est très régulière ; Java utilise simplement le nom du champ comme nom du getter.
- Pour le champ a, le getter sera a().
- Java utilisera votre définition si vous définissez manuellement un getter qui correspond aux conventions de nommage. Dans le cas contraire, Java créera automatiquement une méthode getter qui respecte correctement les conventions. Le getter non standard ne fera pas une grande différence.
- Les champs qui soutiennent les composants d'un enregistrement sont toujours implicitement privés et ne sont accessibles que par l'intermédiaire d'une méthode d'acquisition.
(Il y a un peu plus à faire, mais cela semble être un bon point d'arrêt).
Ces propriétés des enregistrements garantissent que toute nouvelle fonctionnalité du langage Java qui utilise les enregistrements, comme le pattern matching, fonctionnera toujours, parce que la spécification du langage elle-même garantit le comportement et la structure des enregistrements.
C'est bien. Dites-moi ce que je peux faire avec.
Correspondance de motifs.
Continue...
Il est assez pénible d'avoir à écrire du code extrêmement imbriqué lorsque vous avez beaucoup de conditions basées sur les types de vos données. Ce problème sera mis en perspective lorsque j'introduirai les types sum plus loin dans l'article. La correspondance de motifs est un moyen de vérifier statiquement (c'est-à-dire au moment de la compilation, lorsque vous écrivez le code) que certains motifs sont présents dans les données que vous traitez.
Regardez l'exemple ci-dessous. Notez que les données contenues dans A sont des instances Record et qu'elles peuvent contenir n'importe quel type d'enregistrement. Nous essayons d'abord d'imprimer le contenu de r à l'aide d'instructions Java if normales, avant de le faire à l'aide de la recherche de motifs par commutation.
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | record A(Record inner) {} record B(char b) {} record SomeOtherRecord() {} Record eitherAorB() { boolean cond1 = ((int)(Math.random() * 100) % 2 == 0); boolean cond2 = ((int)(Math.random() * 100) % 2 == 0); return cond1 ? new A(cond2 ? new A(null) : new B('e')) : new B('f'); // returns either A or B. } void main() { var r = eitherAorB(); String oldJavaResult = ""; if (r instanceof A) { var inner = ((A)r).inner(); // We have to cast it... if (inner instanceof B) { oldJavaResult = String.valueOf(((B)inner).b()); } else if (inner instanceof SomeOtherRecord) { oldJavaResult = null; } } else if (r instanceof B) { oldJavaResult = String.valueOf((B)r.b()); } else { oldJavaResult = "r does not match any pattern"; } System.out.println("With the old method: \"" + oldJavaResult + "\""); // The type is Record. var result = switch (r) { case A(B(char a)) -> String.valueOf(a); // Destructuring! case A(SomeOtherRecord(/* ... */)) -> { // handle it. yield null; } case B(char b) -> String.valueOf(b); default -> "r does not match any pattern"; }; System.out.println(result.toString()); } |
Si vous souhaitez l'essayer vous-même, voici comment procéder. Copiez ce code dans un main.java et exécutez-le avec :
Code : | Sélectionner tout |
java --enable-preview --source 21 main.java
Dans l'exemple précédent, nous avons pu utiliser la recherche de motifs pour passer d'un type d'enregistrement à l'autre. Mettons cela de côté une seconde et parlons de la façon dont nous gérons les choix en Java.
Choix, choix...
À quoi pensez-vous lorsque vous avez besoin d'un ensemble restreint d'alternatives parmi lesquelles choisir ? Les enums Java répondent à ce besoin ; ils sont composés d'un ensemble de variantes statiques et ne peuvent pas modifier les données qu'ils contiennent.
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public enum Color { RED(255, 0, 0), GREEN(0, 255, 0), BLUE(0, 0, 255); public final int red; public final int green; public final int blue; Color(int red, int green, int blue) { this.red = red; this.green = green; this.blue = blue; } } |
Imaginons maintenant un problème différent. Vous voulez différentes représentations des couleurs telles que RGB, HSL et CMYK. Vous pourriez peut-être créer un enum pour cela ?
Code : | Sélectionner tout |
1 2 3 4 5 6 | public enum ColorRepresentation { RGB, HSL, YUV, CMYK } |
Code : | Sélectionner tout |
1 2 3 4 5 6 | class Color { public final ColourRepresentation repr; public final Number val1; public final Number val2; public final Number val3; } |
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | public abstract class Color {} public class RGB extends Color { private int red; private int green; private int blue; public RGB(int red, int green, int blue) { this.red = red; this.green = green; this.blue = blue; } @Override public String toString() { return "RGB Color: (" + red + ", " + green + ", " + blue + ")"; } } public class CMYK extends Color { private double cyan; private double magenta; private double yellow; private double black; public CMYK(double cyan, double magenta, double yellow, double black) { this.cyan = cyan; this.magenta = magenta; this.yellow = yellow; this.black = black; } @Override public String toString() { return "CMYK Color: (" + cyan + "%, " + magenta + "%, " + yellow + "%, " + black + "%)"; } } public class YUV extends Color { private int y; private int u; private int v; public YUV(int y, int u, int v) { this.y = y; this.u = u; this.v = v; } @Override public String toString() { return "YUV Color: (Y=" + y + ", U=" + u + ", V=" + v + ")"; } } public class HSL extends Color { private double hue; private double saturation; private double lightness; public HSL(double hue, double saturation, double lightness) { this.hue = hue; this.saturation = saturation; this.lightness = lightness; } @Override public String toString() { return "HSL Color: (H=" + hue + ", S=" + saturation + "%, L=" + lightness + "%)"; } } |
Pour résoudre ce problème, nous pourrions faire plusieurs choses :
- Rendre toutes les variantes final.
- Bien que cela soit utile, cela n'empêche pas la création de nouvelles variantes directement à partir de Color, puisqu'il est impossible de rendre Color lui-même final.
- S'assurer que toute la logique interne a toujours un cas par défaut pour les variantes non reconnues.
- Cela réduira l'extensibilité de la bibliothèque, mais si ce n'est pas un objectif, cela aidera.
- Mais ce sera aussi beaucoup plus sujet aux erreurs ; si une seule partie de la logique oublie de prendre en compte le cas par défaut, il y aura des problèmes.
- On peut aussi utiliser des classes ou des interfaces scellées et faire d'une pierre deux coups.
Somme des types
Les classes scellées de Java 17 permettent de créer des modèles de conception basés sur le concept des Somme des types. Alors que la plage de valeurs d'un type produit est le produit des plages de valeurs des types qui le composent, la plage de valeurs d'un type somme est la somme. Mais que signifie le fait que la plage de valeurs soit une somme ?
Les Somme des types encodent le fait qu'un type peut être n'importe lequel de ses constituants à la fois. Ils sont également connus sous le nom de types d'union étiquetés car, dans la théorie des types, ils sont généralement représentés comme un type dont la plage de valeurs est l'ensemble d'union de ses composants, où chaque type composant est "étiqueté" avec une étiquette.
Vous pourriez exprimer un type somme de la manière suivante si vous utilisiez la notation pseudo-théorique des types :
Code : | Sélectionner tout |
T = A + B + C
Code : | Sélectionner tout |
T = { x | x ∈ A ⋃ B ⋃ C }
Code : | Sélectionner tout |
1 2 3 4 5 | union MyUnion { int intValue; double doubleValue; char charValue; }; |
Code : | Sélectionner tout |
1 2 3 4 5 | union MyUnion myUnion; myUnion.intValue = 42; printf("Integer value: %d\n", myUnion.intValue); myUnion.doubleValue = 3.14159; printf("Double value: %lf\n", myUnion.doubleValue); |
Ceci met en évidence le défaut le plus important des unions en C ; il n'y a pas de moyen intégré de savoir quelle variante est contenue dans une valeur d'union sans information externe. Nous avons donc besoin d'un discriminant externe pour déterminer ce qui se trouve à l'intérieur. Le polymorphisme de Java peut le faire pour nous avec instanceof. Mais la hiérarchie des classes de Java est trop ouverte ; il n'y a aucun moyen de restreindre le nombre de variantes d'une "union" Java.
Ce dont nous avons besoin, ce sont des unions balisées, et c'est exactement ce que les types scellés nous permettent de représenter. Le modificateur sealed existe pour indiquer clairement que vous ne pouvez pas étendre une classe sealed au-delà des classes autorisées à en hériter. Cela permet au développeur de contrôler la manière dont les utilisateurs interagissent avec l'API de sa bibliothèque.
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | public sealed class Color permits RGB, CMYK, YUV, HSL { // Common properties or methods for all color representations } final class RGB extends Color { private final int red; private final int green; private final int blue; public RGB(int red, int green, int blue) { this.red = red; this.green = green; this.blue = blue; } // Additional methods or properties specific to RGB } final class CMYK extends Color { private final double cyan; private final double magenta; private final double yellow; private final double black; public CMYK(double cyan, double magenta, double yellow, double black) { this.cyan = cyan; this.magenta = magenta; this.yellow = yellow; this.black = black; } // Additional methods or properties specific to CMYK } final class YUV extends Color { private final int y; private final int u; private final int v; public YUV(int y, int u, int v) { this.y = y; this.u = u; this.v = v; } // Additional methods or properties specific to YUV } final class HSL extends Color { private final double hue; private final double saturation; private final double lightness; public HSL(double hue, double saturation, double lightness) { this.hue = hue; this.saturation = saturation; this.lightness = lightness; } // Additional methods or properties specific to HSL } |
Cette hiérarchie de classes est verrouillée. Aucune nouvelle classe ne peut hériter de Color, qu'elle soit située dans le même paquetage ou non.
Si votre hiérarchie de classes commence par une classe sealed, vous devez marquer tous les héritiers (directs ou indirects) comme sealed, non-sealed ou final. Si une classe qui hérite n'a pas ces modificateurs, il s'agit d'une erreur de compilation.
Voici la signification de chacun de ces modificateurs :
- sealed - La classe ne peut pas être héritée à moins que le nom de l'héritier ne soit mentionné après permits.
- non-sealed - La classe peut être héritée normalement. Utile pour contrôler la portée des comportements personnalisés.
- final - La classe est une "feuille" dans l'arbre d'héritage ; vous ne pouvez plus l'étendre.
Ces modificateurs peuvent être utilisés pour contrôler exactement comment les classes d'une API peuvent se comporter et comment elles sont utilisées. Cela permet d'éviter les situations où la seule chose qui empêche tout d'exploser sont des règles qui ne peuvent être appliquées que par les développeurs qui acceptent de ne pas faire la mauvaise chose.
Imaginons que nous écrivions un code pour gérer les couleurs en fonction de leur format. Nous disposons déjà d'un moyen agréable et limité de garantir qu'il n'y aura pas d'irrégularités. Mais comment structurer le code qui gère les instances de classe scellées ?
Les versions antérieures de Java ne nous laissaient pas beaucoup de choix ; le mieux que nous puissions faire est une échelle if-else :
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | Color color = new RGB(255, 0, 0); if (color instanceof RGB) { RGB rgb = (RGB) color; // ... } else if (color instanceof CMYK) { CMYK cmyk = (CMYK) color; // ... } else if (color instanceof YUV) { YUV yuv = (YUV) color; // ... } else if (color instanceof HSL) { HSL hsl = (HSL) color; // ... } else { System.out.println("Unknown color type"); } |
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 | if (color instanceof RGB rgb) { // ... } else if (color instanceof CMYK cmyk) { // ... } else if (color instanceof YUV yuv) { // ... } else if (color instanceof HSL hsl) { // ... } else { System.out.println("Unknown color type"); } |
Rust vous permettrait de faire cela, par exemple :
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | let color = Color::RGB(255, 0, 0); match color { Color::RGB(red, green, blue) => { // ... } Color::CMYK(cyan, magenta, yellow, black) => { // ... } Color::YUV(y, u, v) => { // ... } Color::HSL(hue, saturation, lightness) => { // ... } } |
La solution consiste à utiliser des interfaces scellées. Elles fonctionnent exactement de la même manière que les classes scellées, sauf que même les enregistrements et les enums peuvent les implémenter.
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | public sealed interface Color permits RGB, CMYK, YUV, HSL { // Common properties or methods for all color representations String getDescription(); } record RGB(int red, int green, int blue) implements Color { public String getDescription() { return "RGB Color: (" + red + ", " + green + ", " + blue + ")"; } } record CMYK(double cyan, double magenta, double yellow, double black) implements Color { public String getDescription() { return "CMYK Color: (" + cyan + "%, " + magenta + "%, " + yellow + "%, " + black + "%)"; } } record YUV(int y, int u, int v) implements Color { public String getDescription() { return "YUV Color: (Y=" + y + ", U=" + u + ", V=" + v + ")"; } } record HSL(double hue, double saturation, double lightness) implements Color { public String getDescription() { return "HSL Color: (H=" + hue + ", S=" + saturation + "%, L=" + lightness + "%)"; } } |
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | Color color = new RGB(255, 0, 0); switch (color) { case RGB(int red, int green, int blue) -> { // red, green and blue are in scope here. } case CMYK(double cyan, double magenta, double yellow, double black) -> { // ... } case YUV(int y, int u, int v) -> { // ... } case HSL hsl -> { // you can also leave the value intact and directly use the pattern matched value. System.out.println(hsl.getDescription()); } case null -> { System.out.println("How did color become null?!"); } } |
Remarque
Java 21 vous permet désormais d'attraper le cas null à l'intérieur des blocs et des expressions switch, de sorte que vous n'avez pas besoin de vérifier la présence de null avant d'arriver à une commutation.
Java 21 vous permet désormais d'attraper le cas null à l'intérieur des blocs et des expressions switch, de sorte que vous n'avez pas besoin de vérifier la présence de null avant d'arriver à une commutation.
Gardes ! Gardes !
On a des clauses de garde ! Vous pouvez attacher des conditions supplémentaires aux bras de commutation qui doivent être vraies pour que le bras soit exécuté ! Les clauses de garde permettent d'exprimer succinctement des conditions plus complexes dans les instructions et expressions de commutation.
Prenons l'exemple d'une situation dans laquelle nous devons appliquer un cas spécial aux couleurs RVB pour lesquelles rouge > 200 est vrai. Auparavant, nous devions placer cette condition à l'intérieur d'une condition if dans le corps du cas correspondant :
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 | switch (color) { case RGB(int red, int green, int blue) -> { if (red > 200) { System.out.println("Very red."); } else { System.out.println("Not that red..."); } } // ... } |
Java 21 nous permet d'intégrer cette condition dans l'étiquette case avec le mot-clé when.
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | switch (color) { // A guarded destructuring case. case RGB(int red, int green, int blue) when red > 200 -> { System.out.println("Very red."); } // You can use guards with direct pattern matching as well. case RGB rgb when rgb.green > 100 -> { System.out.println("Sort of green..."); // ... } // this case is needed to preserve exhaustivity. case RGB rgb -> { System.out.println("Not that red..."); // ... } // ... } |
Remarque
Java continuera à faire correspondre avec empressement le cas évalué comme vrai en premier, donc assurez-vous de placer les cas les plus spécifiques (gardés ou non) en premier, suivis par les cas moins spécifiques.
Java continuera à faire correspondre avec empressement le cas évalué comme vrai en premier, donc assurez-vous de placer les cas les plus spécifiques (gardés ou non) en premier, suivis par les cas moins spécifiques.
Nous avons maintenant une nouvelle classe d'exceptions à gérer. Plus précisément, java.lang.MatchException
Que se passe-t-il lorsqu'une correspondance de motifs ne fonctionne pas correctement ? Prenons le cas d'une mauvaise implémentation d'un getter d'enregistrement :
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 | record R(int i) { public int i() { // bad (but legal) accessor method for i return i / 0; } } static void exampleAnR(R r) { switch(r) { case R(var i): System.out.println(i); // i's accessor always throws! } } |
La JEP 441 stipule ce qui suit :
(Une méthode d'accès à un enregistrement qui lève toujours une exception est très irrégulière, et un commutateur de modèle exhaustif qui lève une MatchException est très inhabituel).
(Une commutation exhaustive sur une énumération n'échoue que si la classe de l'énumération est modifiée après la compilation de la commutation, ce qui est très inhabituel).
Code : | Sélectionner tout |
1 2 3 4 5 6 | static void example(Object obj) { switch (obj) { case R r when (r.i / 0 == 1): System.out. println("It's an R!"); default: break; } } |
Conclusion
Dans cet article, on a vu un certain nombre de choses que Java 21 nous permet de faire.
Traduit par Jade Emy avec l'aimable autorisation de l'auteur Raghav Shankar : Java 21's pattern matching could actually convince me to touch Java again.
Et vous ?
Quel est votre avis sur le sujet ?
Voir aussi :
Java 21 devrait inclure des collections séquencées et des modèles de chaînes de caractères. Le framework des collections de Java manque d'un type de collection qui représente une séquence
JDK 21 : la First Release Candidate est disponible, la version finale est annoncée pour le 19/09/2023
Java conserve sa popularité dans un paysage qui change, soutient New Relic à travers son récent rapport qui se penche sur l'état de l'écosystème du langage