
Sealed Classes (deutsch: versiegelte Klassen) und Interfaces waren die große Neuerung in Java 17. In diesem Artikel erfährst du:
- Was sind versiegelte Klassen und Interfaces?
- Wie genau funktionieren versiegelte Klassen und Interfaces?
- Wofür brauchen wir sie?
- Warum sollte man die Erweiterbarkeit einer Klassenhierarchie einschränken?
- Wie arbeiten sie mit Records und Pattern Matching zusammen?
Fangen wir an mit einem Beispiel …
Ausgangslage: Beispiel-Klassenhierarchie
Ausgangspunkt sei die folgende Klassenhierarchie:

Hier der Java-Quellcode zu dem Beispiel:
public class Shape { ... }
public class Circle extends Shape { ... }
public class Rectangle extends Shape { ... }
public class Square extends Shape { ... }
public class WeirdShape extends Shape { ... }
public class TranspRectangle extends Rectangle { ... }
public class FilledRectangle extends Rectangle { ... }
Für gewöhnlich können alle Entwickler:innen diese Klassenhierarchie an allen Stellen erweitern. Eine erweiterte Struktur könnte wie folgt aussehen (die hinzugekommenen Klassen sind hellgelb gefärbt):

Nun kann es sein, dass wir die Erweiterung unserer Klassenhierarchie einschränken wollen. Zum Beispiel könnten wir festlegen wollen, dass Entwickler:innen ausschließlich die Klasse WeirdShape erweitern dürfen.
Warum könnten wir das wollen, und wie können wir das tun?
Warum die Erweiterbarkeit einer Klassenhierarchie einschränken?
Es kann mehrere Gründe geben, warum wir die freie Erweiterbarkeit unserer Klassenhierarchie einschränken wollen:
- Wir wollen den inneren Zustand einer Klasse bzw. einer Hierarchie von Klassen schützen und diesen nicht durch eine abgeleitete Klasse auf inkonsistente Weise manipulieren lassen.
- Wir wollen interne Objekte, deren Thread-Sicherheit durch unsere Klasse bzw. Klassenhierarchie garantiert wird, vor Veröffentlichung schützen, damit die Thread-Sicherheit nicht durch fremden Code gefährdet werden kann.
- Wir wollen sicherstellen, dass das Liskovsche Substitutionsprinzip (LSP) nicht verletzt wird. Das heißt: Wir wollen nicht, dass jemand eine abgeleitete Klasse implementiert, die den API-Vertrag der Elternklasse bricht.
- Wir wollen uns die Vollständigkeitsanalyse bei „Pattern Matching for switch“ zunutze machen.
Da wir jetzt die Gründe kennen, aus denen wir eine Klassenhierarchie einschränken wollen, kommen wir zur nächsten Frage: Wie können wir das tun?
Klassenhierarchie versiegeln – Schritt für Schritt
Die erste Möglichkeit kennen wir bereits …
Klassenhierarchie einschränken mit „final“
Indem wir Klassen als final markieren, können wir deren Erweiterung generell verhindern.
Eine zweite Möglichkeit wäre es, eine Klasse als package-private zu markieren, um nur Unterklassen innerhalb desselben Pakets zu erlauben. Das hätte allerdings zur Folge, dass die Oberklasse nicht mehr außerhalb des Pakets sichtbar wäre, was in den meisten Fällen unerwünscht ist.
Versuchen wir einmal, final in unserem Beispiel einzusetzen. Wir markieren die Klassen Circle, TranspRectangle, FilledRectangle und Square als final (zur Erinnerung: WeirdShape soll als einzige Klasse erweiterbar bleiben).
Die Erweiterungsmöglichkeiten unserer Klassenhierarchie werden dadurch wie in folgendem Klassendiagramm dargestellt eingeschränkt:

Um die Übersicht zu verbessern, habe ich in der folgenden Grafik die durchgestrichenen Kästchen unter den finalen Klassen entfernt:

Damit sind wir auf einem guten Weg, aber noch lange nicht am Ziel. Was jetzt? Shape und Rectangle können wir offensichtlich nicht final machen, denn von diesen Klassen sollen ja andere erben.
An dieser Stelle kommen die Sealed Classes zum Einsatz …
Klassenhierarchie versiegeln mit „sealed“ und „permits“
Mit „Sealed Classes“ können wir eine sogenannte „versiegelte Klassenhierarchie“ implementieren. Das funktioniert wie folgt:
- Wir markieren die Klasse, deren Unterklassen wir einschränken wollen, mit dem Keyword
sealed. - Mit dem Keyword
permitslisten wir die erlaubten Unterklassen auf.
Wir erweitern den Code der Klassen Shape und Rectangle wie folgt:
public sealed class Shape permits Circle, Square, Rectangle, WeirdShape { ... }
public sealed class Rectangle extends Shape
permits TranspRectangle, FilledRectangle { ... }
Wir sagen mit diesem Code das Folgende aus:
- Die Klasse
Shapedarf nur durch die KlassenCircle,Square,RectangleundWeirdShapeerweitert werden. - Die Klasse
Rectangledarf nur durch die KlassenTranspRectangleundFilledRectangleerweitert werden.
Das folgende Klassendiagramm zeigt die durch sealed und permits hinzugekommenen Einschränkungen:

Der Übersicht halber, hier noch einmal ohne die durchgestrichenen Klassen:

Es sieht so aus, als hätten wir damit unser Ziel erreicht. Doch ein Schritt fehlt noch …
Versiegelte Klassenhierarchie mit „non-sealed“ öffnen
Durch die bisherigen Änderungen sieht unser Code wie folgt aus:
public sealed class Shape permits Circle, Square, Rectangle, WeirdShape { ... }
public final class Circle extends Shape { ... }
public sealed class Rectangle extends Shape
permits TranspRectangle, FilledRectangle { ... }
public final class Square extends Shape { ... }
public class WeirdShape extends Shape { ... }
public final class TranspRectangle extends Rectangle { ... }
public final class FilledRectangle extends Rectangle { ... }
Wenn wir versuchen, diesen Code zu kompilieren, erhalten wir die folgende Fehlermeldung:
$ javac *.java
WeirdShape.java:3: error: sealed, non-sealed or final modifiers expected
public class WeirdShape extends Shape {
^
Um versehentliche Öffnungen der versiegelten Klassenhierarchie zu verhindern, müssen alle erlaubten Unterklassen mit sealed, non-sealed oder final markiert werden.
Unsere Klasse WeirdShape soll erweiterbar sein, d. h. an dieser Klasse soll die Versiegelung geöffnet werden. Dazu müssen wir diese Klasse mit non-sealed markieren:
public non-sealed class WeirdShape extends Shape { ... }
Unsere abschließende Klassenhierarchie sieht damit so aus:

Prüfen, ob eine Klasse versiegelt ist und welche Klassen sie erweitern können
Seit Java 17 hat die Klasse Class zwei neue Methoden:
isSealed()– gibttruezurück, wenn diese Klasse oder dieses Interface versiegelt ist.getPermittedSubclasses()– gibt ein Array der Klassen oder Interfaces zurück, die diese Klasse bzw. dieses Interface erweitern dürfen, bzw.null, wenn diese Klasse/dieses Interface nicht versiegelt ist.
Für unser Beispiel sieht das so aus:
System.out.println(Shape.class.isSealed());
System.out.println(Arrays.toString(Shape.class.getPermittedSubclasses()));
System.out.println(WeirdShape.class.isSealed());
System.out.println(Arrays.toString(WeirdShape.class.getPermittedSubclasses()));
Die Ausgabe:
true
[class Circle, class Square, class Rectangle, class WeirdShape]
false
null
Du siehst: Die mit non-sealed geöffnete Klasse WeirdShape gilt nicht als versiegelt.
Übrigens: Auch ein Enum ist implizit versiegelt, sobald mindestens eine seiner Konstanten einen eigenen Klassenrumpf hat – isSealed() liefert dafür true.
Besonderheiten
Bei der Verwendung von versiegelten Klassenhierarchien gilt es einige Besonderheiten zu beachten.
Versiegelung innerhalb einer „Compilation Unit“
Das Keyword permits kann weggelassen werden, wenn innerhalb einer Quelldatei („Compilation Unit“) von einer versiegelten Klasse abgeleitete Unterklassen definiert werden. Diese gelten dann als „implizit deklarierte zulässige Unterklassen“ („implicitly declared permitted subclasses“).
In folgendem Beispiel ist ChildInSameCompilationUnit eine solche Unterklasse; das permits-Keyword darf daher weggelassen werden:
public sealed class SealedParentWithoutPermits {
public final class ChildInSameCompilationUnit extends SealedParentWithoutPermits {
// ...
}
}
Wo erlaubte Unterklassen liegen dürfen
Die in permits genannten Klassen müssen im selben Modul liegen wie die versiegelte Klasse. Liegt die versiegelte Klasse im unbenannten Modul – also auf dem Classpath statt auf dem Modulpath –, müssen die erlaubten Unterklassen sogar im selben Paket liegen.
Liegt z. B. WeirdShape auf dem Classpath in einem anderen Paket als Shape, meldet der Compiler an der permits-Liste von Shape:
class Shape in unnamed module cannot extend a sealed class in a different package
Außerdem muss jede in permits genannte Klasse direkt von der versiegelten Klasse erben. Eine Enkelklasse in der permits-Liste lehnt der Compiler mit „invalid permits clause“ ab.
Lokale Klassen
Lokale Klassen (also innerhalb von Methoden definierte Klassen) dürfen versiegelte Klassen nicht erweitern.
Der folgende Code zeigt eine lokale Klasse, die eine nicht versiegelte Klasse erweitert. Dieser Code ist gültig:
public class NonSealedParent {
public void doSomethingSmart() {
class LocalChild extends NonSealedParent { // Allowed
// ...
}
// ...
}
}
Wenn die äußere Klasse allerdings versiegelt ist, darf die lokale Klasse nicht von ihr erben:
public sealed class SealedParent {
public void doSomethingSmart() {
class LocalChild extends SealedParent { // Not allowed
// ...
}
// ...
}
}
Der Compiler meldet hier:
local classes must not extend sealed classes
Dasselbe gilt für anonyme Klassen: Auch sie dürfen weder eine versiegelte Klasse noch ein versiegeltes Interface erweitern.
instanceof-Tests mit versiegelten Klassen
Bei instanceof-Tests prüft der Compiler, ob die Klassenhierarchie es zulässt, dass der Check jemals true ergeben kann. Ist das nicht der Fall, meldet der Compiler einen „incompatible types“-Fehler, wie z. B. in folgendem Code:
Number n = getNumber();
if (n instanceof String) { // Not allowed
// ...
}
Ein Number-Objekt kann nie eine Instanz eines Strings sein. Der Compiler meldet daher:
incompatible types: Number cannot be converted to String
Auch die Informationen aus versiegelten Klassenhierarchien werden mit in diese Prüfung aufgenommen. Was das bedeutet, erkläre ich am besten an einem Beispiel:
Nehmen wir an, wir haben ein Interface A und eine Klasse B:
interface A {}
class B {}
Damit ist folgender Check gültig:
public boolean isAaB(A a) {
return a instanceof B;
}
Wie kann dieser Check true ergeben? Indem wir eine Klasse C definieren, die von B erbt und A implementiert:
class C extends B implements A {}
Der Check isAaB(new C()) ergibt dann true.
Nun versiegeln wir das Interface A und erlauben als Unterklasse nur noch AChild; Klasse B lassen wir unverändert:
sealed interface A permits AChild {}
final class AChild implements A {}
class B {}
Der Compiler erkennt nun, dass ein Objekt vom Typ A niemals auch eine Instanz von B sein kann. Entsprechend wird die Prüfung if (a instanceof B) ab sofort mit folgendem Compilerfehler quittiert:
incompatible types: A cannot be converted to B
Contextual Keywords
Die Einführung neuer Keywords wie sealed, non-sealed, permits (oder auch yield aus den Switch Expressions) warf bei den JDK-Entwickler:innen folgende Frage auf: Was soll mit bestehendem Code passieren, der diese Keywords als Methoden- oder Variablennamen verwendet?
Da Java einen hohen Wert auf Abwärtskompatibilität legt, entschied man sich dazu, bestehenden Code möglichst nicht zu beeinträchtigen. Möglich machen das sogenannte „Contextual Keywords“ – Keywords, die nur in einem bestimmten Kontext eine Bedeutung haben.
Die Begriffe sealed und permits z. B. sind solche „Contextual Keywords“ und haben nur im Kontext der Klassendefinition eine Bedeutung. In anderen Kontexten können sie als Methoden- oder Variablenname verwendet werden. Folgendes ist also erlaubt:
public void sealed() {
int permits = 5;
}
Als Klassenname hingegen sind sealed und permits seit Java 15 verboten. Für class sealed {} meldet der Compiler:
error: 'sealed' not allowed here
as of release 15, 'sealed' is a restricted type name and cannot be used for type declarations
Vollständigkeitsanalyse bei „Pattern Matching for switch“
„Pattern Matching for switch“ wurde in Java 17 als Preview-Feature vorgestellt und ist seit Java 21 final. In Kombination mit diesem Feature erlauben versiegelte Klassen eine Vollständigkeitsanalyse, d. h. der Compiler kann prüfen, ob ein switch-Statement oder -Ausdruck alle möglichen Fälle abdeckt.
Hier eine kleine Klassenhierarchie mit einem versiegelten Interface als Wurzel:
public sealed interface Color permits Red, Blue {}
public final class Red implements Color {}
public final class Blue implements Color {}
„Pattern Matching for switch“ ermöglicht Code wie den folgenden:
Color color = getColor();
switch (color) {
case Red _ -> ...
case Blue _ -> ...
}
Den Unterstrich _ setzen wir, weil wir die Variable im jeweiligen Zweig nicht brauchen – solche unbenannten Patterns sind seit Java 22 final.
Der Compiler erkennt, dass das Objekt color nur eine Instanz von Red oder Blue sein kann; das switch-Statement ist also vollständig und benötigt keinen default-Fall.
Ein weiterer Vorteil ist, dass wir bei einer eventuellen späteren Erweiterung der Klassenhierarchie vom Compiler auf den fehlenden switch-Fall hingewiesen werden.
Erweitern wir unsere Klassenhierarchie um die Farbe Grün (nicht vergessen: die permits-Liste von Color erweitern):
public sealed interface Color permits Red, Blue, Green {}
public final class Red implements Color {}
public final class Blue implements Color {}
public final class Green implements Color {}
Beim Versuch, das switch-Statement zu kompilieren, bricht der Compiler jetzt mit folgender Fehlermeldung ab:
$ javac SwitchTest.java
SwitchTest.java:6: error: the switch statement does not cover all possible input values
switch (color) {
^
missing patterns:
Green _
Du siehst: Der Compiler nennt sogar das fehlende Pattern.
Der Compiler kann uns also bei versiegelten Klassenhierarchien helfen, unvollständige switch-Statements oder -Ausdrücke – eine häufige Fehlerursache bei der Erweiterung von Klassenhierarchien – zu vermeiden.
Sealed Interfaces, Records und Record Patterns
Ihre volle Wirkung entfaltet die Vollständigkeitsanalyse in Kombination mit Records. Records sind implizit final und brauchen deshalb keinen weiteren Modifier, um ein versiegeltes Interface zu implementieren:
public sealed interface Shape permits Circle, Square {}
public record Circle(double radius) implements Shape {}
public record Square(double side) implements Shape {}
Mit den seit Java 21 finalen Record Patterns zerlegt der switch die Records direkt in ihre Komponenten:
double area(Shape shape) {
return switch (shape) {
case Circle(double radius) -> Math.PI * radius * radius;
case Square(double side) -> side * side;
};
}
Auch dieser switch-Ausdruck braucht keinen default-Fall. Kommt eine dritte Form hinzu, meldet der Compiler jeden switch ohne default-Fall, der sie noch nicht behandelt.
Fazit
Versiegelte Klassen wurden nach zwei Preview-Runden in Java 15 und Java 16 durch JDK Enhancement Proposal 409 in Java 17 finalisiert. Sie erlauben uns, eine Klassenhierarchie vor ungewünschten Erweiterungen zu schützen.
Zusammen mit „Pattern Matching for switch“, das seit Java 21 final ist, ermöglichen sie darüber hinaus eine Vollständigkeitsanalyse.
Ich empfehle versiegelte Interfaces überall dort, wo eine Hierarchie eine feste Menge von Fällen abbildet, etwa Ergebnis- oder Ereignistypen: Kommt ein Fall hinzu, zeigt dir der Compiler jeden switch ohne default-Fall, der ihn noch nicht behandelt.
Sealed Classes wurden zusammen mit anderen neuen Sprachfeatures wie Records, Switch Expressions, Text Blocks und Pattern Matching in Projekt Amber entwickelt.
Was soll ich als Nächstes vertiefen? Das erfahre ich am besten durch dein Feedback: Eine Bewertung auf meinem ProvenExpert-Profil zeigt mir, welche Themen dir wichtig sind – und motiviert mich, weitere Artikel zu schreiben.




