JEP 409 - Sealed Classes
Classes et interfaces scellées pour un meilleur contrôle de l'héritage
En résumé
Les sealed classes (classes scellées) permettent de contrôler précisément quelles classes peuvent hériter d'une classe ou implémenter une interface. C'est un intermédiaire entre les classes ouvertes (publiques) et les classes finales : vous définissez explicitement une liste fermée de sous-classes autorisées.
Syntaxe de base
// Classe scellée : seules Circle, Rectangle et Triangle peuvent l'étendre
public sealed class Shape
permits Circle, Rectangle, Triangle {
private final String nom;
protected Shape(String nom) {
this.nom = nom;
}
public String getNom() {
return nom;
}
}
// Les sous-classes autorisées doivent être final, sealed ou non-sealed
public final class Circle extends Shape {
private final double rayon;
public Circle(double rayon) {
super("Cercle");
this.rayon = rayon;
}
public double aire() {
return Math.PI * rayon * rayon;
}
}
public final class Rectangle extends Shape {
private final double largeur, hauteur;
public Rectangle(double largeur, double hauteur) {
super("Rectangle");
this.largeur = largeur;
this.hauteur = hauteur;
}
public double aire() {
return largeur * hauteur;
}
}
public final class Triangle extends Shape {
private final double base, hauteur;
public Triangle(double base, double hauteur) {
super("Triangle");
this.base = base;
this.hauteur = hauteur;
}
public double aire() {
return 0.5 * base * hauteur;
}
}
Motivations
1. Domaines fermés
Certains concepts ont naturellement un ensemble limité de variantes :
- Formes géométriques : cercle, rectangle, triangle
- Types de réponses HTTP : succès, erreur client, erreur serveur
- Expressions mathématiques : nombre, addition, multiplication
- États d'une machine : en attente, en cours, terminé, erreur
2. Exhaustivité dans le pattern matching
Le compilateur peut vérifier que tous les cas sont couverts :
double calculerAire(Shape shape) {
// Le compilateur vérifie l'exhaustivité : pas besoin de default !
return switch (shape) {
case Circle c -> Math.PI * c.rayon() * c.rayon();
case Rectangle r -> r.largeur() * r.hauteur();
case Triangle t -> 0.5 * t.base() * t.hauteur();
// Pas de 'default' nécessaire
};
}
Interfaces scellées
public sealed interface JsonValue
permits JsonString, JsonNumber, JsonBoolean, JsonNull, JsonArray, JsonObject {
}
public record JsonString(String value) implements JsonValue {}
public record JsonNumber(double value) implements JsonValue {}
public record JsonBoolean(boolean value) implements JsonValue {}
public record JsonNull() implements JsonValue {}
public record JsonArray(List items) implements JsonValue {}
public record JsonObject(Map fields) implements JsonValue {}
Modificateurs des sous-classes
Les sous-classes d'une sealed class doivent être :
final - Pas d'extension possible
public final class Circle extends Shape {
// Personne ne peut étendre Circle
}
sealed - Extension contrôlée
public sealed class Polygon extends Shape
permits Triangle, Square, Pentagon {
// Seulement ces 3 classes peuvent étendre Polygon
}
non-sealed - Extension libre
public non-sealed class CustomShape extends Shape {
// N'importe qui peut étendre CustomShape
}
Exemples pratiques
Exemple 1 : Système de réponses HTTP
public sealed interface HttpResponse
permits Success, ClientError, ServerError {}
public record Success(int code, String body) implements HttpResponse {}
public record ClientError(int code, String message) implements HttpResponse {}
public record ServerError(int code, Exception exception) implements HttpResponse {}
// Traitement exhaustif
String traiter(HttpResponse response) {
return switch (response) {
case Success(int code, String body) ->
"Succès (" + code + ") : " + body;
case ClientError(int code, String msg) ->
"Erreur client " + code + " : " + msg;
case ServerError(int code, Exception ex) ->
"Erreur serveur " + code + " : " + ex.getMessage();
};
}
Exemple 2 : Expressions mathématiques
public sealed interface Expr
permits Const, Add, Mult, Neg {}
public record Const(int value) implements Expr {}
public record Add(Expr left, Expr right) implements Expr {}
public record Mult(Expr left, Expr right) implements Expr {}
public record Neg(Expr expr) implements Expr {}
// Évaluateur
int eval(Expr expr) {
return switch (expr) {
case Const(int v) -> v;
case Add(Expr l, Expr r) -> eval(l) + eval(r);
case Mult(Expr l, Expr r) -> eval(l) * eval(r);
case Neg(Expr e) -> -eval(e);
};
}
// Utilisation
Expr calcul = new Add(new Const(5), new Mult(new Const(3), new Const(4)));
int resultat = eval(calcul); // 5 + (3 * 4) = 17
Exemple 3 : Machine à états
public sealed interface State
permits Idle, Loading, Success, Error {}
public record Idle() implements State {}
public record Loading(int progress) implements State {}
public record Success(String data) implements State {}
public record Error(String message) implements State {}
// Component UI (pseudo-code)
String renderUI(State state) {
return switch (state) {
case Idle _ -> "";
case Loading(int p) -> "";
case Success(String data) -> "" + data + "";
case Error(String msg) -> "" + msg + "";
};
}
Règles et contraintes
Localité
Les sous-classes autorisées doivent être dans le même module ou package :
// Dans le même fichier (si possible)
sealed interface Result permits Success, Failure {}
record Success(String data) implements Result {}
record Failure(String error) implements Result {}
// Ou dans le même package
package com.example.shapes;
public sealed class Shape permits Circle, Rectangle {
// Circle et Rectangle doivent être dans com.example.shapes
}
Héritage transitif
sealed interface Animal permits Mammal, Bird {}
sealed interface Mammal extends Animal permits Dog, Cat {}
final class Dog implements Mammal {}
final class Cat implements Mammal {}
sealed interface Bird extends Animal permits Eagle, Sparrow {}
final class Eagle implements Bird {}
final class Sparrow implements Bird {}
Avantages
Pour le développeur
- ✅ Modélisation précise des domaines fermés
- ✅ Pattern matching exhaustif garanti par le compilateur
- ✅ Auto-documentation : la hiérarchie est explicite
- ✅ Refactoring sûr : le compilateur détecte les cas manquants
Pour la maintenance
- ✅ Empêche l'extension non contrôlée
- ✅ Facilite l'évolution (ajout/retrait de variantes)
- ✅ Réduit les bugs liés aux cas non gérés
Sealed vs Final vs Public
| Type | Extension | Usage |
|---|---|---|
public class |
Illimitée | API publique, frameworks |
sealed class |
Contrôlée (liste explicite) | Hiérarchies fermées, ADT |
final class |
Interdite | Classes utilitaires, sécurité |
Bonnes pratiques
✅ À faire
- Utiliser sealed pour les hiérarchies de types fixes (ADT, états, etc.)
- Combiner avec records pour des données immutables
- Profiter du pattern matching exhaustif
- Documenter pourquoi la hiérarchie est fermée
❌ À éviter
- Ne pas sceller des API qui doivent être extensibles
- Éviter de sceller si la liste peut évoluer fréquemment
- Ne pas confondre avec les énumérations (préférer enum pour des constantes)