JEP 409 - Sealed Classes

Classes et interfaces scellées pour un meilleur contrôle de l'héritage

Numéro JEP

409

Statut

Final

Introduit dans

Java 17 (LTS)

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)

Ressources