W3docs

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 eigene permits-Liste.
  • non-sealed — öffnet die Hierarchie wieder; jeder darf nun Triangle erweitern. 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 permits aufgeführt werden – nur unmittelbare Untertypen.
  • Jeder erlaubte Untertyp muss einen Modifikator wählen: final, sealed oder non-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, ohne final explizit 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

java— editable, runs on the server

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.

Übungen

Übung
Was bewirkt das Deklarieren einer Klasse als `sealed`?
Was bewirkt das Deklarieren einer Klasse als `sealed`?
Was this page helpful?