Zum Inhalt springen

Sealed Classes und Interfaces in Java

Ein flacher Messingring auf Pflaumenrot, ganz ausgefüllt von drei Glaskeilen in Rubinrot, Bernstein und Türkis; wo sie sich in der Mitte treffen, sitzt ein rotes Wachssiegel

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:

Sealed Classes Beispiel – Ausgangslage
Sealed Classes Beispiel – Ausgangslage

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):

Sealed Classes Beispiel – Erweiterungsmöglichkeiten ohne Versiegelung
Sealed Classes Beispiel – Erweiterungsmöglichkeiten ohne Versiegelung

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:

Klassenhierarchie mit „final“ einschränken
Klassenhierarchie mit „final“ einschränken

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

Klassenhierarchie mit „final“ einschränken
Klassenhierarchie mit „final“ einschränken

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 permits listen 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 Shape darf nur durch die Klassen Circle, Square, Rectangle und WeirdShape erweitert werden.
  • Die Klasse Rectangle darf nur durch die Klassen TranspRectangle und FilledRectangle erweitert werden.

Das folgende Klassendiagramm zeigt die durch sealed und permits hinzugekommenen Einschränkungen:

Klassenhierarchie mit „sealed“ und „permits“ einschränken
Klassenhierarchie mit „sealed“ und „permits“ einschränken

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

Klassenhierarchie mit „sealed“ und „permits“ einschränken
Klassenhierarchie mit „sealed“ und „permits“ einschränken

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:

Versiegelte Klassenhierarchie mit „non-sealed“ öffnen
Versiegelte Klassenhierarchie mit „non-sealed“ öffnen

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() – gibt true zurü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.

👉 Bewertung abgeben

Dieses Thema im eigenen Code?

Du hast den Artikel gelesen – im Training arbeitet ihr damit. 2 Tage lang gehen wir die Themen an euren eigenen Projekten durch, statt an konstruierten Beispielen.

Praxisnah, verständlich und direkt auf euren Projektalltag übertragbar. Statt Theorie vermittle ich Prinzipien, die euch helfen, Code langfristig besser, wartbarer und performanter zu schreiben.

Java 17 SchulungAlle Trainings ansehen

Werde ein:e bessere:r Java-Entwickler:in

Mit meinem kostenlosen Newsletter bleibst du vorn. Modernes Java: neue Versionen & Features, Performance und JVM-Insights – 1x im Monat.

Suche