W3docs

Les types scellés en Java en profondeur

Modélisez des hiérarchies de types fermées en Java avec les classes et interfaces scellées, notamment avec le filtrage par motif.

Les classes et interfaces scellées (finalisées dans Java 17) permettent à un type de déclarer exactement quels autres types peuvent l'étendre ou l'implémenter. Au lieu d'une hiérarchie ouverte que n'importe qui peut sous-classer, vous définissez un ensemble fermé sur lequel le compilateur peut raisonner. Cette garantie unique — ces types et seulement eux — est ce qui permet le filtrage par motif exhaustif et rend les hiérarchies de classes orientées données sûres à modéliser.

Un type scellé est le partenaire naturel des enregistrements. Les enregistrements vous donnent les données ; le scellement vous donne l'ensemble fermé de cas. Ensemble, ils apportent les types de données algébriques (le « type somme » que vous connaissez peut-être de Kotlin, Rust ou Scala) en Java pur, et ils changent le comportement d'un switch sur une hiérarchie.

Ce chapitre explique comment sceller un type avec permits, le choix obligatoire final / sealed / non-sealed que chaque sous-type doit faire, pourquoi une hiérarchie fermée permet un switch exhaustif sans default, et comment le scellement se combine avec la déconstruction d'enregistrements et les motifs gardés. Il s'appuie sur les interfaces et l'héritage.

Sceller un type avec permits

Un type devient scellé avec le modificateur sealed et une clause permits qui liste tous les sous-types directs autorisés. Aucune autre classe ne peut rejoindre la hiérarchie, même dans le même package. Les sous-types autorisés doivent être accessibles au type scellé et, dans le module sans nom, résider dans le même package (ou le même module).

public sealed interface Payment
        permits Cash, Card, BankTransfer {}

public record Cash(int amount) implements Payment {}
public record Card(String number, int amount) implements Payment {}
public record BankTransfer(String iban, int amount) implements Payment {}

Si un sous-type se trouve dans le même fichier source, la clause permits est facultative — le compilateur l'infère à partir du fichier. Vous devez écrire permits explicitement uniquement lorsque les sous-types se trouvent dans des fichiers séparés.

// Same file: permits is inferred, so it can be omitted.
sealed interface Expr {
    record Num(int value) implements Expr {}
    record Add(Expr left, Expr right) implements Expr {}
}

La règle final, sealed et non-sealed

Chaque sous-type autorisé doit lui-même indiquer comment sa partie de la hiérarchie est fermée. Le compilateur impose un choix : chaque sous-type direct doit être déclaré final, sealed ou non-sealed. Il n'existe pas d'option « ne rien faire » — omettre le modificateur est une erreur de compilation.

ModificateurSignification pour le sous-type
finalLe sous-type ne peut pas être étendu davantage. Les enregistrements sont implicitement final.
sealedLe sous-type est lui-même fermé et fournit sa propre liste permits.
non-sealedLe sous-type rouvre la hiérarchie — n'importe qui peut l'étendre à nouveau.
public sealed class Shape permits Circle, Polygon, Freeform {}

public final class Circle extends Shape {}          // closed here
public sealed class Polygon extends Shape            // closed, but to a set
        permits Triangle, Rectangle {}
public non-sealed class Freeform extends Shape {}    // reopened: any subclass allowed

public final class Triangle extends Polygon {}
public final class Rectangle extends Polygon {}

non-sealed est la trappe de sortie : elle permet à une branche d'une hiérarchie autrement fermée de rester ouverte à l'extension. Utilisez-la avec parcimonie, car elle abandonne la garantie d'exhaustivité pour cette branche.

Pourquoi le scellement permet un switch exhaustif

L'avantage de fermer une hiérarchie est que le compilateur connaît la liste complète des cas. Un switch sur un type scellé qui couvre chaque sous-type autorisé est exhaustif, donc vous n'écrivez pas de branche default. Mieux encore, si quelqu'un ajoute ultérieurement un nouveau sous-type autorisé, chaque switch non exhaustif cesse de compiler — le compilateur vous signale le code qui a oublié le nouveau cas.

sealed interface Payment permits Cash, Card, BankTransfer {}
record Cash(int amount) implements Payment {}
record Card(String number, int amount) implements Payment {}
record BankTransfer(String iban, int amount) implements Payment {}

static String fee(Payment p) {
    return switch (p) {                 // no default needed
        case Cash c         -> "no fee";
        case Card c         -> "2% card fee";
        case BankTransfer b -> "flat fee";
    };
}

Supprimez le cas BankTransfer et le code ne compilera pas : "the switch expression does not cover all possible input values." Cette vérification à la compilation est la raison principale de sceller une hiérarchie.

Enregistrements, déconstruction et motifs gardés

Comme les sous-types autorisés sont généralement des enregistrements, vous pouvez combiner le scellement avec les motifs de déconstruction d'enregistrements et les motifs gardés (when) — voir le filtrage par motif pour la fonctionnalité complète. La déconstruction lie les composants de l'enregistrement directement dans l'étiquette case ; un garde ajoute une condition booléenne. L'ordre compte : les cas gardés plus spécifiques doivent précéder le cas de repli non gardé pour le même type.

sealed interface Shape permits Circle, Rectangle {}
record Circle(double radius) implements Shape {}
record Rectangle(double width, double height) implements Shape {}

static String describe(Shape s) {
    return switch (s) {
        case Circle(double r) when r > 10        -> "big circle";
        case Circle(double r)                    -> "circle r=" + r;
        case Rectangle(double w, double h) when w == h -> "square";
        case Rectangle(double w, double h)       -> "rectangle";
    };
}

Le compilateur considère toujours cela comme exhaustif : chaque sous-type autorisé est mis en correspondance par au moins une étiquette non gardée, donc le switch entier est total même si certaines étiquettes sont gardées.

Un exemple complet

L'exemple exécutable ci-dessous regroupe tout : une interface Shape scellée avec trois sous-types enregistrements, un switch exhaustif pour l'aire, un switch de déconstruction gardé pour la description, et un aperçu des métadonnées de scellement par réflexion. Il n'utilise que le JDK, il s'exécute donc tel quel.

java— editable, runs on the server

Ce que l'exécution nous apprend :

  • Le switch d'aire n'a pas de branche default — comme Shape est scellé, couvrir les trois enregistrements est déjà exhaustif.
  • describe affiche big circle r=12.0 uniquement pour le cercle de rayon 12, prouvant que le garde when r > 10 est testé avant l'étiquette Circle non gardée.
  • Le rectangle de côté 5 affiche square side=5.0, montrant que le garde w == h l'emporte sur le cas Rectangle ordinaire qui le suit.
  • L'aire totale (525,96) est accumulée sur chaque sous-type enregistrement, confirmant qu'une seule boucle polymorphique gère toute la hiérarchie fermée.
  • Shape.class.isSealed() retourne true et getPermittedSubclasses() liste Circle, Rectangle et Triangle — l'ensemble permits survit dans les métadonnées d'exécution.

Exercice

Pratique
Pourquoi un switch exhaustif sur une interface scellée peut-il omettre la branche default ?
Pourquoi un switch exhaustif sur une interface scellée peut-il omettre la branche default ?
Was this page helpful?