Cette article est une traduction avec annotations de la documentation officielle de la JEP 507, disponible en anglais ici : https://openjdk.org/jeps/507
- Introduction et Contexte
- Les Objectifs Fondamentaux
- Les Limitations Actuelles et Leurs Conséquences
- Le Concept Central : La Sécurité des Conversions
- Extension de l'Opérateur instanceof
- Patterns de Types Primitifs : Syntaxe et Sémantique
- Extension de Switch : Vers l'Universalité
- Implications et Travaux Futurs
- Conclusion : Une Évolution Cohérente
Introduction et Contexte
La JEP 507 représente la troisième itération d’une fonctionnalité majeure de Java qui vise à uniformiser l’utilisation des types primitifs dans le pattern matching. Cette évolution s’inscrit dans la continuité des améliorations apportées au langage Java pour rendre la manipulation des données plus expressive et plus sûre.
Note d’annotation : Une JEP (Java Enhancement Proposal) est un document technique qui décrit une nouvelle fonctionnalité ou amélioration proposée pour le langage Java. Le statut « Preview » signifie que cette fonctionnalité est disponible pour tests et retours, mais n’est pas encore définitive.
La JEP 507 est incluse dans la release Java 25 (LTS).
Les Objectifs Fondamentaux
Cette enhancement poursuit plusieurs objectifs interconnectés qui méritent d’être compris dans leur ensemble :
Uniformisation de l’exploration des données : L’objectif principal consiste à permettre l’utilisation de patterns de types pour tous les types Java, qu’ils soient primitifs ou de référence. Actuellement, cette capacité est limitée aux types de référence, créant une asymétrie dans le langage.
Alignement conceptuel : La proposition vise à aligner les type patterns avec l’opérateur instanceof
, et cet opérateur avec le concept de cast sécurisé. Cette cohérence améliore la prévisibilité du comportement du langage.
Élimination des risques de perte d’information : En fournissant des constructions faciles à utiliser, cette JEP élimine les risques associés aux casts non sécurisés qui peuvent entraîner une perte silencieuse de données.
Les Limitations Actuelles et Leurs Conséquences
Pattern Matching dans Switch : Une Restriction Frustrante
Actuellement, le pattern matching pour switch
ne supporte que les type patterns qui spécifient un type de référence. Cette limitation force les développeurs à écrire du code verbeux et moins expressif.
Considérons cet exemple typique :
// Code actuel - verbeux et répétitif switch (x.getStatus()) { case 0 -> "okay"; case 1 -> "warning"; case 2 -> "error"; default -> "unknown status: " + x.getStatus(); // Répétition de x.getStatus() }
Avec les améliorations proposées, ce code deviendrait :
// Code amélioré - plus expressif et réutilisable switch (x.getStatus()) { case 0 -> "okay"; case 1 -> "warning"; case 2 -> "error"; case int i -> "unknown status: " + i; // La valeur est directement disponible }
Annotation pédagogique : La différence clé ici est que case int i
capture la valeur dans la variable i
, éliminant le besoin de rappeler x.getStatus()
et rendant le code plus maintenable.
Record Patterns : L’Asymétrie des Conversions
Les record patterns illustrent parfaitement l’incohérence actuelle du langage. Prenons l’exemple d’une représentation JSON :
sealed interface JsonValue { record JsonString(String s) implements JsonValue { } record JsonNumber(double d) implements JsonValue { } record JsonObject(Map<String, JsonValue> map) implements JsonValue { } }
La création d’un JsonNumber
accepte naturellement une conversion implicite :
// Ceci fonctionne - conversion automatique int vers double var json = new JsonObject(Map.of("name", new JsonString("John"), "age", new JsonNumber(30))); // 30 est un int
Mais la déconstruction force une approche asymétrique :
// Code actuel - nécessite un cast manuel potentiellement dangereux if (json instanceof JsonObject(var map) && map.get("name") instanceof JsonString(String n) && map.get("age") instanceof JsonNumber(double a)) { int age = (int)a; // Cast obligatoire et potentiellement lossy ! }
L’amélioration proposée permettrait une approche cohérente :
// Code amélioré - conversion sécurisée automatique if (json instanceof JsonObject(var map) && map.get("name") instanceof JsonString(String n) && map.get("age") instanceof JsonNumber(int a)) { // Conversion sécurisée // 'a' est directement utilisable comme int // Si la conversion n'est pas possible, le pattern ne matche pas }
Le Concept Central : La Sécurité des Conversions
Conversions Exactes : Le Fondement de la Sécurité
La JEP introduit le concept crucial de « conversion exacte » qui détermine quand une conversion peut être effectuée sans perte d’information. Cette notion se décline en deux catégories :
Conversions inconditionnellement exactes : Ces conversions sont garanties sûres au moment de la compilation, quel que soit la valeur. Par exemple, convertir un byte
vers un int
ne peut jamais causer de perte d’information.
Conversions conditionnellement exactes : Ces conversions nécessitent une vérification à l’exécution. Par exemple, convertir un long
vers un int
peut être exact si la valeur tient dans un int
, mais pas sinon.
Tableau de Compatibilité des Conversions
Le document fournit un tableau détaillé des conversions permises entre types primitifs. Les symboles utilisés ont des significations précises :
≈
: Conversion identité (même type)ɛ
: Conversion inconditionnellement exacteω
: Conversion widening (élargissement) primitiveη
: Conversion narrowing (rétrécissement) primitive—
: Aucune conversion permise
Annotation explicative : Une conversion « widening » va d’un type plus petit vers un type plus grand (par exemple int
vers long
), tandis qu’une conversion « narrowing » fait l’inverse et peut potentiellement perdre de l’information.
Extension de l’Opérateur instanceof
Philosophie : instanceof comme Précondition du Cast Sécurisé
L’extension d’instanceof
aux types primitifs suit une logique fondamentale : instanceof
devrait répondre à la question « Serait-il sûr et utile de caster cette valeur vers ce type ? »
Cette question est encore plus pertinente pour les types primitifs que pour les types de référence. Pour les types de référence, un cast non sécurisé lance une ClassCastException
claire. Pour les types primitifs, un cast non sécurisé peut silencieusement corrompre les données.
Exemples Pratiques d’Extension
byte b = 42; b instanceof int; // true (inconditionnellement exact) int i = 42; i instanceof byte; // true (exact pour cette valeur) int i = 1000; i instanceof byte; // false (pas exact - dépasse la capacité d'un byte) int i = 16_777_217; // 2^24 + 1 i instanceof float; // false (perte de précision) i instanceof double; // true (inconditionnellement exact)
Annotation technique : Le nombre 16_777_217
est significatif car il dépasse la précision d’un float
(qui utilise 24 bits pour la mantisse), démontrant ainsi la détection de perte de précision.
Patterns de Types Primitifs : Syntaxe et Sémantique
Simplification du Code Existant
La nouvelle syntaxe transforme du code verbeux et sujet aux erreurs :
// Ancien style - verbeux et dangereux int i = 1000; if (i instanceof byte) { // false - ne peut pas être converti exactement byte b = (byte)i; // Cast potentiellement lossy ! // ... utilisation de b ... }
En code concis et sûr :
// Nouveau style - concis et sûr if (i instanceof byte b) { // ... utilisation directe de b sans perte d'information ... }
Prédicats de Définition Sémantique
La sémantique des type patterns repose sur trois prédicats fondamentaux :
Applicabilité : Détermine si un pattern est légal au moment de la compilation. Auparavant limitée aux types exactement identiques, elle s’étend maintenant à tous les types pour lesquels un cast est possible.
Inconditionnalité : Détermine si un pattern applicable matchera toutes les valeurs possibles à l’exécution, ne nécessitant aucune vérification runtime.
Matching : Détermine si une valeur peut être castée exactement vers le type du pattern, couvrant à la fois les exceptions potentielles et les pertes d’information.
Extension de Switch : Vers l’Universalité
Support des Types Primitifs Manquants
L’extension de switch
couvre les types primitifs restants : long
, float
, double
, et boolean
, ainsi que leurs types boxés correspondants.
Switch sur Boolean : Alternative au Ternaire
// Nouveau : switch sur boolean avec des statements startProcessing(OrderStatus.NEW, switch (user.isLoggedIn()) { case true -> user.id(); case false -> { log("Unrecognized user"); yield -1; } });
Annotation pédagogique : Cette approche est particulièrement utile quand la branche false
nécessite plusieurs statements, ce qui n’est pas possible avec l’opérateur ternaire.
Switch sur Long : Gestion des Grandes Constantes
long v = ...; switch (v) { case 1L -> ...; case 2L -> ...; case 10_000_000_000L -> ...; // Constantes très grandes case 20_000_000_000L -> ...; case long x -> ... x ...; // Pattern général }
Exhaustivité et Dominance
Exhaustivité : Un switch
doit couvrir toutes les valeurs possibles. Avec les types primitifs, une nouvelle règle s’ajoute : un type pattern T t rend exhaustif un switch sur un wrapper type W si T est inconditionnellement exact sur le type primitif correspondant.
Dominance : Un pattern domine un autre s’il matche toutes les valeurs que l’autre matche. Cette règle s’étend maintenant aux types primitifs, prévenant les cas inaccessibles.
Implications et Travaux Futurs
Vers les Constant Patterns
Cette régularisation ouvre la voie aux « constant patterns », permettant d’utiliser des constantes dans les record patterns :
record Box(short s) {} Box b = ... switch (b) { case Box(42) -> ... // Futur : pattern avec constante case Box(int i) -> ... // Pattern général }
Annotation prospective : Cette évolution future permettrait d’écrire case Box(42)
comme équivalent à case Box(int i) when i == 42
, rendant le pattern matching encore plus expressif.
Conclusion : Une Évolution Cohérente
La JEP 507 représente une évolution majeure vers la cohérence et l’expressivité du langage Java. En éliminant les asymétries entre types primitifs et types de référence dans le pattern matching, elle simplifie l’apprentissage du langage et réduit les sources d’erreurs.
Cette troisième preview, identique aux précédentes, témoigne de la maturité de la proposition et de sa stabilité conceptuelle. Elle s’inscrit dans la continuité des améliorations apportées à switch
(enum switch en Java 5, string switch en Java 7) et représente une étape logique vers un langage plus uniforme et plus puissant.
L’impact de ces changements se fera sentir particulièrement dans le code de manipulation de données, où la combinaison de sécurité de type et d’expressivité facilitera l’écriture de programmes plus robustes et maintenables.