1. Syntaxe des classes sealed : à quoi cela ressemble
Commençons par un problème classique de la POO en Java : la hiérarchie d’héritage ouverte. En Java, n’importe qui peut étendre votre classe, sauf si elle est déclarée final. C’est pratique, mais cela mène parfois à des surprises — par exemple, vous ne pouvez pas savoir à l’avance quelles classes seront des sous-classes de la vôtre, et donc vous ne pouvez pas garantir que vous avez couvert tous les cas dans un switch ou un if-else.
Par conséquent, lorsque vous traitez des objets par type (par exemple via le pattern matching dans un switch), vous êtes obligé d’ajouter une branche default « au cas où » : et si quelqu’un, quelque part, créait une nouvelle sous-classe ?
Les classes sealed résolvent ce problème : elles permettent de restreindre explicitement la liste des classes filles. La hiérarchie devient fermée et contrôlée, et votre code — plus prévisible et plus sûr.
Syntaxe de base
Les classes sealed sont apparues avec Java 17. Elles se déclarent à l’aide du modificateur sealed, et la liste des sous-classes autorisées est indiquée avec le mot-clé permits :
public sealed class Shape permits Circle, Rectangle, Square {
// Comportement commun pour toutes les figures
}
Ici, nous avons déclaré la classe Shape, et seules les classes Circle, Rectangle et Square peuvent en être des sous-classes directes. Personne d’autre ne pourra étendre Shape — le compilateur ne le permettra pas.
Important : toutes les classes mentionnées dans permits doivent être déclarées dans le même fichier ou être visibles du compilateur (généralement dans le même package). D’ailleurs, si tous les sous-types sont déclarés dans le même fichier que la classe sealed, on peut omettre complètement permits — le compilateur le déduira.
Exemple :
// Tout dans un seul fichier - permits n’est pas obligatoire
public sealed class Shape {
}
final class Circle extends Shape {}
final class Rectangle extends Shape {}
Exigences pour les sous-classes
Chaque sous-classe doit indiquer explicitement son statut :
- être final (interdit tout héritage ultérieur),
- ou être sealed (et continuer à restreindre l’héritage),
- ou être non-sealed (autorise l’héritage, lève les restrictions).
Exemple :
public sealed class Shape permits Circle, Rectangle, Square {}
public final class Circle extends Shape {}
public sealed class Rectangle extends Shape permits FilledRectangle, EmptyRectangle {}
public non-sealed class Square extends Shape {}
- Circle — final, pas d’héritage supplémentaire possible.
- Rectangle — lui-même sealed, n’autorise que deux sous-classes.
- Square — non-sealed, n’importe qui peut l’étendre.
Exemple minimal
public sealed class Animal permits Dog, Cat {}
public final class Dog extends Animal {}
public final class Cat extends Animal {}
Essayez de déclarer une nouvelle classe public class Wolf extends Animal {} — vous obtiendrez une erreur de compilation :
class Wolf is not allowed to extend sealed class Animal
2. Utilisation des classes sealed: où et pourquoi utiliser
Pattern matching et switch
Les classes sealed s’accordent particulièrement bien avec le pattern matching dans switch. Si le compilateur connaît toutes les sous-classes possibles, il peut vérifier que vous avez traité chaque variante, et il n’exigera même pas de branche default.
public sealed interface Result permits Success, Error {}
public final class Success implements Result {
public final String data;
public Success(String data) { this.data = data; }
}
public final class Error implements Result {
public final String message;
public Error(String message) { this.message = message; }
}
public class Main {
public static void main(String[] args) {
Result result = new Success("Hourra !");
switch (result) {
case Success s -> System.out.println("Succès : " + s.data);
case Error e -> System.out.println("Erreur : " + e.message);
}
}
}
Le compilateur sait qu’il n’existe pas d’autres variantes pour Result et n’exige pas default. Si vous oubliez d’en traiter une, il vous le rappellera immédiatement.
À noter : à partir de Java 21, si dans un switch toutes les variantes ne sont pas traitées, vous obtiendrez une erreur de compilation. Dans les versions antérieures (17–20), default peut être nécessaire, mais l’IDE avertira tout de même d’une couverture incomplète.
Sécurité et contrôle
Les classes sealed permettent au développeur de contrôler entièrement la hiérarchie. C’est particulièrement important pour les modèles métier où l’ensemble des variantes doit être fixe (par exemple l’état d’une commande : New, Paid, Cancelled).
Faciliter la maintenance et l’évolution du code
Quand vous connaissez toutes les variantes de sous-classes, il est plus simple d’ajouter de nouvelles fonctionnalités, de maintenir le code et de faire du refactoring. L’IDE est aussi « au courant » de toutes les variantes et peut aider avec l’autocomplétion et l’analyse.
3. Exemples pratiques d’utilisation des classes sealed
Exemple 1 : Figures géométriques
public sealed interface Shape permits Circle, Rectangle, Square {}
public final class Circle implements Shape {
public final double radius;
public Circle(double radius) { this.radius = radius; }
}
public final class Rectangle implements Shape {
public final double width, height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
}
public final class Square implements Shape {
public final double side;
public Square(double side) { this.side = side; }
}
On peut maintenant utiliser switch avec le pattern matching en toute sécurité :
Shape shape = new Circle(5);
switch (shape) {
case Circle c -> System.out.println("Cercle de rayon " + c.radius);
case Rectangle r -> System.out.println("Rectangle " + r.width + "x" + r.height);
case Square s -> System.out.println("Carré de côté " + s.side);
}
Exemple 2 : Transactions financières
public sealed interface Transaction permits Deposit, Withdraw, Transfer {}
public final class Deposit implements Transaction {
public final double amount;
public Deposit(double amount) { this.amount = amount; }
}
public final class Withdraw implements Transaction {
public final double amount;
public Withdraw(double amount) { this.amount = amount; }
}
public final class Transfer implements Transaction {
public final double amount;
public final String toAccount;
public Transfer(double amount, String toAccount) {
this.amount = amount;
this.toAccount = toAccount;
}
}
Vous pouvez maintenant être certain, dans le gestionnaire, que vous n’avez oublié aucun type de transaction :
Transaction tx = new Transfer(100, "ACC123");
switch (tx) {
case Deposit d -> System.out.println("Dépôt : " + d.amount);
case Withdraw w -> System.out.println("Retrait : " + w.amount);
case Transfer t -> System.out.println("Virement : " + t.amount + " vers " + t.toAccount);
}
Exemple 3 : Hiérarchie mixte avec non-sealed
public sealed class Notification permits EmailNotification, SmsNotification, PushNotification {}
public final class EmailNotification extends Notification {}
public non-sealed class SmsNotification extends Notification {}
public final class PushNotification extends Notification {}
// Désormais, n’importe qui peut hériter de SmsNotification
public class ViberNotification extends SmsNotification {}
4. Particularités, limitations et subtilités des classes sealed
Exigences relatives aux modificateurs
- Une classe sealed doit énumérer explicitement toutes ses sous-classes via permits.
- Toutes les sous-classes doivent être soit final, soit sealed, soit non-sealed.
- Les sous-classes doivent être déclarées dans le même fichier ou être visibles du compilateur.
Classes sealed abstraites
Une classe sealed peut être abstraite, un interface, ou une classe ordinaire. Par exemple :
public sealed abstract class Expr permits Const, Add, Mul {}
Compatibilité avec d’autres modificateurs
- On ne peut pas déclarer une classe sealed en tant que final ou non-sealed.
- interface peut également être sealed (et c’est très pratique!).
Utilisation avec les classes record
Les classes record peuvent être des sous-types d’une classe sealed si elles sont déclarées comme final (par défaut, record est toujours final) :
public sealed interface Expr permits Const, Add, Mul {}
public record Const(int value) implements Expr {}
public record Add(Expr left, Expr right) implements Expr {}
public record Mul(Expr left, Expr right) implements Expr {}
5. Points utiles
Classes sealed et pattern matching : quel lien
Le principal atout des classes sealed — un pattern matching exhaustif. Ça paraît impressionnant, mais c’est simple. Le compilateur connaît toutes les variantes possibles, et vous pouvez écrire un switch sans branche default :
Expr expr = ...;
switch (expr) {
case Const c -> ...
case Add a -> ...
case Mul m -> ...
}
Si vous oubliez de traiter une variante, le compilateur refusera de construire le projet — c’est très pratique et sûr.
Applications dans des cas réels
Où les classes sealed sont-elles vraiment utiles ? Partout où vous avez un ensemble de variantes fixe :
- Résultat d’une opération : Success, Error (comme dans l’exemple ci-dessus).
- État d’une commande : New, Paid, Cancelled.
- Arbres syntaxiques abstraits (AST) pour des analyseurs.
- Réponses d’API : Ok, NotFound, Error.
- Événements dans le système : UserLoggedIn, UserLoggedOut, UserRegistered.
6. Erreurs courantes avec les classes sealed
Erreur n°1 : oublié d’indiquer toutes les sous-classes dans permits.
Si vous n’avez pas listé toutes les sous-classes nécessaires via permits, le compilateur se plaindra immédiatement. Par exemple, si vous avez écrit permits Circle, Rectangle mais oublié Square alors qu’une telle classe existe — vous obtiendrez une erreur.
Erreur n°2 : la sous-classe n’est ni final, sealed ni non-sealed.
Si la sous-classe n’est pas déclarée avec le modificateur requis, le compilateur émettra une erreur : « Class must be either final, sealed or non-sealed ».
Erreur n°3 : la sous-classe est déclarée dans un autre fichier et n’est pas visible du compilateur.
Toutes les classes listées dans permits doivent être accessibles au compilateur — soit dans le même fichier, soit dans le même package.
Erreur n°4 : toutes les variantes ne sont pas traitées dans switch.
Si vous utilisez une classe sealed dans un switch avec pattern matching et oubliez de traiter une variante, le compilateur refusera de construire le projet. C’est bien — vous ne passerez à côté d’aucun cas.
Erreur n°5 : tentative d’hériter d’une classe sealed non listée dans permits.
Si vous tentez de créer une sous-classe non indiquée dans permits, vous obtiendrez l’erreur : « is not allowed to extend sealed class ».
Erreur n°6 : tentative d’utiliser les classes sealed sur d’anciennes versions du JDK.
Les classes sealed sont apparues seulement avec Java 17. Si vous essayez de les utiliser dans une version plus ancienne, vous obtiendrez une erreur de compilation ou ne pourrez tout simplement pas construire le projet.
GO TO FULL VERSION