Classes scellées en Java
Limitez les classes pouvant étendre ou implémenter un type Java avec les classes scellées et la clause permits.
Une classe ou interface scellée restreint les types autorisés à l'étendre ou à l'implémenter à une liste fixe et nommée de sous-types. final signifie « personne ne peut m'étendre ». sealed signifie « seules ces classes spécifiques le peuvent. » Cela vous donne une hiérarchie fermée — le compilateur connaît toute la famille à l'avance, ce qui permet un switch exhaustif et une modélisation disciplinée de formes « parmi N possibilités ».
Sans scellement, une abstract class Shape est ouverte au monde entier : n'importe qui ayant accès au type peut écrire class Banana extends Shape. Avec sealed, l'auteur de Shape déclare exactement quels sous-types existent, et en ajouter un nécessite de modifier le parent.
La syntaxe de base
Une classe scellée liste ses sous-types autorisés avec permits :
public sealed class Shape
permits Circle, Square, Triangle {
// common state and behavior
}Chaque sous-type autorisé doit lui-même déclarer ce qu'il fait du scellement — l'un parmi final, sealed (avec sa propre liste permits), ou non-sealed :
public final class Circle extends Shape { /* leaf */ }
public final class Square extends Shape { /* leaf */ }
public non-sealed class Triangle extends Shape { /* re-opens the door */ }final— aucune sous-classe supplémentaire ; c'est une feuille dans la hiérarchie.sealed— étend le même modèle ; possède sa propre listepermits.non-sealed— ouvre à nouveau la porte ; n'importe qui peut désormais étendreTriangle. Utile quand vous voulez une famille fermée au niveau supérieur avec une branche ouverte.
Un type scellé sans modificateur sur un sous-type est une erreur de compilation — le compilateur vous force à choisir.
Interfaces scellées
Les interfaces suivent les mêmes règles et constituent généralement le choix le plus naturel pour modéliser des familles de cas :
public sealed interface Result<T>
permits Success, Failure {}
public record Success<T>(T value) implements Result<T> {}
public record Failure<T>(String message) implements Result<T> {}Combinées avec des records, vous obtenez quelque chose de proche du « type somme » ou « union étiquetée » des langages fonctionnels — une liste fermée d'alternatives nommées, chacune avec ses propres données.
Même module, même package (ou permits explicite)
Les sous-types autorisés doivent être accessibles à la déclaration scellée au moment de la compilation. La configuration la plus simple consiste à placer la classe scellée et ses sous-types autorisés dans le même fichier source — vous pouvez alors même omettre permits, car le compilateur l'infère :
public sealed interface Tree {
record Leaf(int value) implements Tree {}
record Node(Tree left, Tree right) implements Tree {}
}S'ils se trouvent dans des fichiers séparés, ils doivent être dans le même package (ou, dans un projet modulaire, le même module), et la clause permits est requise.
Le bénéfice : switch exhaustif
Le compilateur connaît tous les sous-types possibles d'un type scellé. Cela permet à switch d'assurer l'exhaustivité sans default :
double area(Shape s) {
return switch (s) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Square q -> q.side() * q.side();
case Triangle t -> 0.5 * t.base() * t.height();
};
}Si vous ajoutez ultérieurement un Hexagon autorisé, ce switch cesse de compiler partout où il apparaît jusqu'à ce que vous gériez le nouveau cas. C'est exactement le filet de sécurité que default détruirait silencieusement.
Règles appliquées par le compilateur
Quelques contraintes sont faciles à enfreindre par inadvertance :
- Chaque sous-type autorisé doit directement étendre ou implémenter le type scellé. Vous ne pouvez pas lister un petit-enfant dans
permits— uniquement les sous-types immédiats. - Chaque sous-type autorisé doit choisir un modificateur :
final,sealedounon-sealed. L'oublier est une erreur de compilation. - Les types autorisés doivent être trouvables au moment de la compilation — même fichier, même package, ou même module nommé. Un type scellé ne peut pas autoriser une classe dans un module non lié.
- Les records sont implicitement
final, donc un record peut être un sous-type autorisé sans écrirefinalvous-même. C'est pourquoi la combinaison interface scellée + records est si élégante.
Le modificateur non-sealed est l'échappatoire délibérée. Utilisez-le quand la majeure partie d'une hiérarchie doit rester fermée mais qu'une branche est un point d'extension prévu :
public sealed interface Vehicle permits Car, Truck, CustomBuild {}
public record Car(int doors) implements Vehicle {}
public record Truck(double tons) implements Vehicle {}
// Re-opened: third parties may extend this branch.
public non-sealed interface CustomBuild extends Vehicle {}Parce que CustomBuild est non-sealed, un switch sur Vehicle nécessite quand même un cas de repli pour lui — le compilateur ne peut plus prouver que cette branche est exhaustive.
Quand sceller
Optez pour le scellement quand l'abstraction est véritablement un ensemble fermé de cas :
- Nœuds d'AST ou d'expressions (
Literal,Add,Multiply...). - Résultats de domaine qui sont « succès ou l'un de ces échecs. »
- Hiérarchies de commandes/événements où chaque consommateur en aval doit traiter chaque cas.
Ne scellez pas les types qui sont des points d'extension — interfaces de plugins, hooks de frameworks, tout ce que les appelants sont censés sous-typer. Les sceller va à l'encontre de leur objectif.
Un exemple complet
La suite
Le scellement verrouille la liste des sous-types. Le prochain chapitre porte sur la question de savoir, à l'exécution, quel sous-type vous avez réellement — l'opérateur instanceof et sa forme moderne avec correspondance de motifs, qui est ce qui rend le switch ci-dessus si concis. Continuez vers Opérateur instanceof en Java.