Java Sealed Classes
Einschränken, welche Klassen einen Typ in Java erweitern oder implementieren dürfen – mit sealed classes und der permits-Klausel.
Eine versiegelte Klasse oder ein versiegeltes Interface schränkt ein, wer sie erweitern oder implementieren darf – auf eine feste, namentlich aufgeführte Liste von Untertypen. final bedeutet „niemand kann mich erweitern." sealed bedeutet „nur diese bestimmten Klassen können es." Es entsteht eine geschlossene Hierarchie – der Compiler kennt die gesamte Familie im Voraus, was erschöpfende switch-Ausdrücke und die disziplinierte Modellierung von „einer von N" Formen ermöglicht.
Ohne Versiegelung ist eine abstract class Shape für alle offen: Jeder mit Zugriff auf den Typ kann class Banana extends Shape schreiben. Mit sealed legt der Autor von Shape genau fest, welche Untertypen existieren, und das Hinzufügen eines neuen Untertyps erfordert die Bearbeitung der übergeordneten Klasse.
Die grundlegende Syntax
Eine versiegelte Klasse listet ihre erlaubten Untertypen mit permits auf:
public sealed class Shape
permits Circle, Square, Triangle {
// common state and behavior
}Jeder erlaubte Untertyp muss selbst angeben, was er mit der Versiegelung macht – entweder final, sealed (mit einer eigenen permits-Liste) oder 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— keine weiteren Unterklassen; das ist ein Blatt in der Hierarchie.sealed— erweitert dasselbe Modell; hat eine eigenepermits-Liste.non-sealed— öffnet die Hierarchie wieder; jeder darf nunTriangleerweitern. Nützlich, wenn man eine geschlossene Top-Level-Familie mit einem offenen Zweig möchte.
Ein versiegelter Typ ohne Modifikator bei einem Untertyp ist ein Kompilierfehler – der Compiler zwingt zur Wahl.
Versiegelte Interfaces
Interfaces folgen denselben Regeln und sind meist die natürlichere Wahl für die Modellierung von Fallmengen:
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> {}In Kombination mit Records erhält man etwas, das dem „Summentyp" oder „Tagged Union" aus funktionalen Sprachen nahekommt – eine geschlossene Liste benannter Alternativen, jede mit eigenen Daten.
Gleiches Modul, gleiches Paket (oder explizites permits)
Die erlaubten Untertypen müssen zur Kompilierzeit für die versiegelte Deklaration zugänglich sein. Die einfachste Lösung ist, die versiegelte Klasse und ihre erlaubten Untertypen in derselben Quelldatei zu platzieren – dann kann permits sogar weggelassen werden, da der Compiler es ableitet:
public sealed interface Tree {
record Leaf(int value) implements Tree {}
record Node(Tree left, Tree right) implements Tree {}
}Befinden sie sich in separaten Dateien, müssen sie im selben Paket sein (oder in einem modularen Projekt im selben Modul), und die permits-Klausel ist erforderlich.
Der Vorteil: erschöpfendes switch
Der Compiler kennt jeden möglichen Untertyp eines versiegelten Typs. Dadurch kann switch ohne default Vollständigkeit erzwingen:
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();
};
}Wenn später ein erlaubtes Hexagon hinzukommt, hört dieses switch überall auf zu kompilieren, bis der neue Fall behandelt wird. Genau das ist das Sicherheitsnetz, das default stillschweigend zerstören würde.
Regeln, die der Compiler durchsetzt
Einige Einschränkungen sind leicht zu übersehen:
- Jeder erlaubte Untertyp muss den versiegelten Typ direkt erweitern oder implementieren. Ein Enkelelement kann nicht in
permitsaufgeführt werden – nur unmittelbare Untertypen. - Jeder erlaubte Untertyp muss einen Modifikator wählen:
final,sealedodernon-sealed. Fehlt einer, ist es ein Kompilierfehler. - Die erlaubten Typen müssen zur Kompilierzeit auffindbar sein – gleiche Datei, gleiches Paket oder dasselbe benannte Modul. Ein versiegelter Typ darf keine Klasse aus einem unabhängigen Modul erlauben.
- Records sind implizit
final, daher kann ein Record ein erlaubter Untertyp sein, ohnefinalexplizit zu schreiben. Das ist der Grund, warum die Kombination aus versiegeltem Interface und Records so elegant ist.
Der non-sealed-Modifikator ist die bewusste Ausstiegsklausel. Verwende ihn, wenn der größte Teil einer Hierarchie geschlossen bleiben soll, aber ein Zweig ein beabsichtigter Erweiterungspunkt ist:
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 {}Da CustomBuild non-sealed ist, benötigt ein switch über Vehicle noch immer einen Fallback dafür – der Compiler kann nicht mehr beweisen, dass dieser Zweig erschöpfend ist.
Wann man versiegeln sollte
Greife auf Versiegelung zurück, wenn die Abstraktion wirklich eine geschlossene Menge von Fällen ist:
- AST- oder Ausdrucksknoten (
Literal,Add,Multiply...). - Domänenergebnisse, die „Erfolg oder einer dieser Fehler" sind.
- Befehls-/Ereignishierarchien, bei denen jeder nachgelagerte Konsument jeden Fall behandeln muss.
Versiegele keine Typen, die Erweiterungspunkte sind – Plugin-Interfaces, Framework-Hooks, alles, was Aufrufer erwartungsgemäß als Untertyp ableiten. Dort würde die Versiegelung ihren Zweck verfehlen.
Ein vollständiges Beispiel
Was kommt als Nächstes
Versiegelung sperrt die Liste der Untertypen fest. Das nächste Kapitel befasst sich damit, zur Laufzeit zu fragen, welchen Untertyp man tatsächlich hat – den instanceof-Operator und seine moderne Pattern-Matching-Form, die den obigen switch so kompakt macht. Weiter zu Java instanceof operator.