Historique
Le construct switch a évolué d'une instruction de contrôle de flux de style C en une expression complète capable de renvoyer des valeurs dans Java 14. Avec Java 17, des classes et des interfaces scellées ont été introduites pour restreindre l'héritage, et la correspondance de motifs pour switch est apparue en tant que fonctionnalité préliminaire, culminant dans la normalisation dans Java 21. Cette évolution a transformé le switch d'une simple table de saut basée sur des constantes discrètes en un mécanisme sophistiqué de correspondance de motifs qui doit garantir la complétude lorsque utilisé comme une expression.
Le Problème
Lorsque switch fonctionne comme une expression (utilisant la syntaxe fléchée -> ou yield), il doit produire une valeur pour chaque entrée possible afin de satisfaire le système de types statiques de Java. Contrairement aux instructions switch traditionnelles qui peuvent passer silencieusement des cas non gérés ou tomber à travers, une expression nécessite une certitude absolue que tous les chemins d'exécution retournent une valeur. Les hiérarchies scellées énumèrent explicitement tous les sous-types autorisés, créant un univers fermé qui rend la couverture totale théoriquement vérifiable à la compilation. Le compilateur doit concilier ce monde fermé avec des motifs ouverts (comme des motifs de type ou des cas nuls) pour garantir qu'aucune MatchException à l'exécution ne se produise en raison de types non couverts.
La Solution
Le compilateur effectue une analyse de dominance et d'exhaustivité pendant la phase d'attribution de la compilation. Il traite la clause permits d'une classe scellée comme un ensemble fini et fermé de types. Pour chaque motif dans le switch, il soustrait les types correspondants de l'univers des types permis. Si un sous-type permis reste non correspondant après le dernier motif, et si aucune clause default inconditionnelle ou motif de type total n'existe, le compilateur rejette le code avec une erreur. Cette analyse respecte les règles de dominance des motifs (où des motifs spécifiques doivent précéder des motifs plus généraux) et génère des machines synthétiques pour gérer les entrées nulles séparément des motifs de type.
sealed interface Payment permits Credit, Debit, Crypto {} record Credit() implements Payment {} record Debit() implements Payment {} record Crypto() implements Payment {} // Erreur à la compilation si le cas Crypto manque double fee = switch (payment) { case Credit c -> 0.02; case Debit d -> 0.01; // Cas Crypto manquant provoque : "l'expression switch ne couvre pas toutes les valeurs possibles" };
Description du problème
Dans un microservice de traitement des paiements, nous avions besoin de calculer des frais en fonction des types d'instruments : Credit, Debit, BankTransfer, et Crypto. Le modèle de domaine utilisait une interface scellée PaymentInstrument autorisant exactement ces quatre implémentations. Un développeur junior a implémenté le calcul des frais à l'aide d'une expression switch mais a omis par inadvertance le cas Crypto, supposant qu'il retournerait implicitement zéro. Lorsque les paiements en cryptomonnaie ont été activés en production, cette omission a causé une MatchException à l'exécution, faisant planter le pipeline de transaction et nécessitant un rollback d'urgence.
Différentes solutions envisagées
Solution A : Clause par défaut de secours
Nous pourrions ajouter une clause default -> 0.0 pour gérer tout instrument non couvert. Cette approche offre une sécurité immédiate en évitant le crash. Cependant, elle obscurcit l'intention commerciale en absorbant silencieusement les types non gérés. Si un nouveau type d'instrument était ultérieurement ajouté à la hiérarchie scellée, la clause par défaut le cacherait dans les calculs de frais, provoquant potentiellement des fuites de revenus ou des violations de conformité.
Solution B : Cartographie des types basée sur des énumérations
Migrer vers une énumération InstrumentType permettrait de vérifier l'exhaustivité à la compilation par l'énumération constante. Cependant, cela crée une taxonomie parallèle nécessitant que chaque instrument de paiement expose des métadonnées de type redondantes. Cela sacrifie la richesse polymorphique des classes scellées, où chaque sous-type porte des champs de données uniques comme des numéros de carte ou des adresses de blockchain, forçant une dénormalisation de données non naturelle.
Solution C : motifs exhaustifs imposés par le compilateur Nous implémentons l'expression switch avec des cas explicites pour les quatre types autorisés, profitant de l'analyse de hiérarchie scellée du compilateur. Cette approche traite les cas manquants comme des erreurs de compilation, forçant des mises à jour du code chaque fois que les autorisations scellées changent. Elle élimine les surprises à l'exécution en déplaçant la vérification vers la phase de construction.
Solution choisie et résultat
Nous avons sélectionné Solution C et configuré le pipeline de construction pour traiter les avertissements du compilateur concernant les expressions switch non exhaustives comme des erreurs fatales. Lorsque l'équipe produit a ensuite ajouté BuyNowPayLater comme cinquième sous-type autorisé, le pipeline CI/CD a immédiatement signalé dix-sept emplacements où les calculs de frais étaient incomplets. Cela a forcé une mise à jour coordonnée à travers les modules fiscaux, de conformité et de comptabilité avant le déploiement, garantissant que le nouvel instrument recevait une logique financière appropriée. Les garanties de compilation ont empêché des valeurs par défaut silencieuses et maintenu la sécurité des types à travers les équipes distribuées.
Comment la gestion de null interagit-elle avec la vérification de l'exhaustivité dans les switches de motifs ?
De nombreux candidats supposent à tort que couvrir tous les sous-types d'une classe scellée satisfait aux exigences d'exhaustivité. Cependant, les expressions switch traitent les sélecteurs null comme distincts des motifs de type ; une clause case null séparée ou un motif total est obligatoire. Sans gestion explicite des null, le compilateur génère une vérification nulle synthétique qui lance une NullPointerException, ce qui signifie que l'expression est techniquement exhaustive pour les types mais pas pour la valeur nulle elle-même.
Pourquoi ajouter une clause par défaut à un switch sur une hiérarchie scellée pourrait potentiellement violer le principe des types scellés ?
Les candidats ajoutent souvent default comme habitude de codage défensive sans reconnaître que cela compromet l'hypothèse du monde fermé des classes scellées. Une clause par défaut correspond à tout type, y compris ceux ajoutés à la liste des permissions dans les futures versions, convertissant effectivement la vérification d'exhaustivité à la compilation en un rattrapage à l'exécution. Cela réintroduit la fragilité exacte que les classes scellées ont été conçues pour éliminer en permettant à de nouveaux types non gérés d'exécuter silencieusement une logique non intentionnelle.
Que se passe-t-il lorsqu'une expression switch sur un type scellé rencontre un type qui est autorisé mais non visible pour le module actuel ?
Ce scénario implique des limites de visibilité où une classe scellée autorise un sous-type privé de paquet dans un autre paquet ou module qui n'est pas exporté à l'unité de compilation actuelle. Le compilateur ne peut pas vérifier l'exhaustivité car l'ensemble complet des types permis est inconnu au site d'utilisation, entraînant une erreur de compilation malgré le fait que tous les types localement visibles soient gérés. Résoudre cela nécessite soit d'ajouter une clause par défaut (défaisant l'exhaustivité), soit d'ajuster les exports de modules JPMS pour rendre les permissions visibles, mettant en évidence l'interaction entre l'accessibilité du module et la correspondance de motifs.