1. Syntax von Sealed-Klassen: so sieht es aus
Beginnen wir mit einem klassischen OOP-Problem in Java: offene Vererbungshierarchien. In normalem Java kann jeder von Ihrer Klasse erben, sofern sie nicht als final deklariert ist. Das ist bequem, führt aber manchmal zu Überraschungen – zum Beispiel können Sie nicht im Voraus wissen, welche Klassen tatsächlich Unterklassen Ihrer Klasse sein werden, und können folglich nicht garantieren, dass Sie alle Fälle in switch oder if-else abgedeckt haben.
Als Ergebnis sind Sie, wenn Sie eine fallbasierte Verarbeitung schreiben (etwa über Pattern Matching in switch), gezwungen, einen default-Zweig „für alle Fälle“ hinzuzufügen: Vielleicht hat irgendwo jemand eine neue Unterklasse erstellt?
Sealed-Klassen lösen dieses Problem: Sie erlauben, die Liste der Unterklassen explizit zu begrenzen. Das macht die Hierarchie geschlossen und kontrolliert, und Ihr Code wird vorhersagbarer und sicherer.
Grundsyntax
Sealed-Klassen gibt es seit Java 17. Sie werden mit dem Modifikator sealed deklariert, und die Liste der erlaubten Unterklassen wird mit dem Schlüsselwort permits angegeben:
public sealed class Shape permits Circle, Rectangle, Square {
// Gemeinsames Verhalten für alle Figuren
}
Hier deklarieren wir die Klasse Shape, und nur die Klassen Circle, Rectangle und Square dürfen direkte Unterklassen davon sein. Niemand sonst kann Shape erweitern – der Compiler lässt das nicht zu.
Wichtig: Alle Klassen, die in permits aufgeführt sind, müssen in derselben Datei deklariert oder für den Compiler sichtbar sein (üblicherweise im selben Paket). Übrigens, wenn alle Unterklassen in derselben Datei wie die Sealed-Klasse deklariert sind, kann man permits ganz weglassen – der Compiler erkennt das selbst.
Beispiel:
// Alles in einer Datei - permits ist optional
public sealed class Shape {
}
final class Circle extends Shape {}
final class Rectangle extends Shape {}
Anforderungen an Unterklassen
Jede Unterklasse muss ihren Status explizit festlegen:
- final sein (verbietet weitere Vererbung),
- oder sealed sein (und die Vererbung weiter einschränken),
- oder non-sealed sein (erlaubt Vererbung, hebt die Einschränkungen auf).
Beispiel:
public sealed class Shape permits Circle, Rectangle, Square {}
public final class Circle extends Shape {}
public sealed class Rectangle extends Shape permits FilledRectangle, EmptyRectangle {}
public non-sealed class Square extends Shape {}
- Circle – endgültig, weitere Vererbung nicht möglich.
- Rectangle – selbst sealed, erlaubt nur zwei Unterklassen.
- Square – non-sealed, jeder kann erweitern.
Minimalbeispiel
public sealed class Animal permits Dog, Cat {}
public final class Dog extends Animal {}
public final class Cat extends Animal {}
Versuchen Sie, eine neue Klasse public class Wolf extends Animal {} zu deklarieren – Sie erhalten einen Compilerfehler:
class Wolf is not allowed to extend sealed class Animal
2. Einsatz von Sealed-Klassen: wo und warum verwenden
Pattern Matching und switch
Sealed-Klassen harmonieren besonders gut mit Pattern Matching in switch. Wenn der Compiler alle möglichen Unterklassen kennt, kann er sicherstellen, dass Sie jede Variante behandeln, und verlangt nicht einmal einen default-Zweig.
public sealed interface Result permits Success, Error {}
public final class Success implements Result {
public final String data;
public Success(String data) { this.data = data; }
}
public final class Error implements Result {
public final String message;
public Error(String message) { this.message = message; }
}
public class Main {
public static void main(String[] args) {
Result result = new Success("Hurra!");
switch (result) {
case Success s -> System.out.println("Erfolg: " + s.data);
case Error e -> System.out.println("Fehler: " + e.message);
}
}
}
Der Compiler weiß, dass es für Result keine weiteren Varianten geben kann, und verlangt default nicht. Wenn Sie eine Variante vergessen, erinnert der Compiler Sie sofort daran.
Beachten Sie: Seit Java 21 erhalten Sie einen Compilerfehler, wenn im switch nicht alle Varianten behandelt werden. In früheren Versionen (17–20) kann default erforderlich sein, aber die IDE warnt dennoch vor unvollständiger Abdeckung.
Sicherheit und Kontrolle
Sealed-Klassen ermöglichen es dem Entwickler, die Hierarchie vollständig zu kontrollieren. Das ist besonders wichtig für Domänenmodelle, bei denen die Menge der Varianten fixiert sein muss (zum Beispiel der Bestellstatus: New, Paid, Cancelled).
Wartbarkeit und Weiterentwicklung vereinfachen
Wenn Sie alle Unterklassen kennen, ist es einfacher, neue Features hinzuzufügen, den Code zu pflegen und zu refaktorisieren. Auch die IDE „kennt“ dann alle Varianten und kann bei Autovervollständigung und Analyse helfen.
3. Praktische Beispiele für Sealed-Klassen
Beispiel 1: Geometrische Figuren
public sealed interface Shape permits Circle, Rectangle, Square {}
public final class Circle implements Shape {
public final double radius;
public Circle(double radius) { this.radius = radius; }
}
public final class Rectangle implements Shape {
public final double width, height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
}
public final class Square implements Shape {
public final double side;
public Square(double side) { this.side = side; }
}
Jetzt kann man switch mit Pattern Matching sicher verwenden:
Shape shape = new Circle(5);
switch (shape) {
case Circle c -> System.out.println("Kreis mit Radius " + c.radius);
case Rectangle r -> System.out.println("Rechteck " + r.width + "x" + r.height);
case Square s -> System.out.println("Quadrat mit Seitenlänge " + s.side);
}
Beispiel 2: Finanztransaktionen
public sealed interface Transaction permits Deposit, Withdraw, Transfer {}
public final class Deposit implements Transaction {
public final double amount;
public Deposit(double amount) { this.amount = amount; }
}
public final class Withdraw implements Transaction {
public final double amount;
public Withdraw(double amount) { this.amount = amount; }
}
public final class Transfer implements Transaction {
public final double amount;
public final String toAccount;
public Transfer(double amount, String toAccount) {
this.amount = amount;
this.toAccount = toAccount;
}
}
Nun kann der Handler sicherstellen, dass kein Transaktionstyp vergessen wurde:
Transaction tx = new Transfer(100, "ACC123");
switch (tx) {
case Deposit d -> System.out.println("Einzahlung: " + d.amount);
case Withdraw w -> System.out.println("Abhebung: " + w.amount);
case Transfer t -> System.out.println("Überweisung: " + t.amount + " an " + t.toAccount);
}
Beispiel 3: Eingeschränkte Hierarchie mit non-sealed
public sealed class Notification permits EmailNotification, SmsNotification, PushNotification {}
public final class EmailNotification extends Notification {}
public non-sealed class SmsNotification extends Notification {}
public final class PushNotification extends Notification {}
// Jetzt kann jeder SmsNotification erweitern
public class ViberNotification extends SmsNotification {}
4. Besonderheiten, Einschränkungen und Feinheiten von Sealed-Klassen
Anforderungen an Modifikatoren
- Eine Sealed-Klasse muss alle Unterklassen explizit via permits angeben.
- Alle Unterklassen müssen entweder final, sealed oder non-sealed sein.
- Unterklassen müssen entweder in derselben Datei deklariert oder für den Compiler sichtbar sein.
Abstrakte Sealed-Klassen
Eine Sealed-Klasse kann abstrakt sein, ein interface oder eine normale Klasse. Zum Beispiel:
public sealed abstract class Expr permits Const, Add, Mul {}
Kompatibilität mit anderen Modifikatoren
- Eine Sealed-Klasse kann nicht als final oder non-sealed deklariert werden.
- Auch ein interface kann sealed sein (und das ist sehr praktisch!).
Verwendung mit Record-Klassen
Record-Klassen können Unterklassen einer Sealed-Klasse sein, wenn sie als final deklariert sind (standardmäßig ist ein record immer final):
public sealed interface Expr permits Const, Add, Mul {}
public record Const(int value) implements Expr {}
public record Add(Expr left, Expr right) implements Expr {}
public record Mul(Expr left, Expr right) implements Expr {}
5. Nützliche Details
Sealed-Klassen und Pattern Matching: wie hängt das zusammen
Das Hauptfeature von Sealed-Klassen ist exhaustives Pattern Matching. Klingt kompliziert, funktioniert einfach. Der Compiler kennt alle möglichen Varianten, und Sie können switch ohne default-Zweig schreiben:
Expr expr = ...;
switch (expr) {
case Const c -> ...
case Add a -> ...
case Mul m -> ...
}
Wenn Sie plötzlich eine Variante nicht behandeln, lässt der Compiler das Projekt nicht bauen – das ist sehr praktisch und sicher.
Einsatz in der Praxis
Wo sind Sealed-Klassen wirklich nützlich? Überall dort, wo Sie eine feste Menge von Varianten haben:
- Operationsergebnis: Success, Error (wie im Beispiel oben).
- Bestellstatus: New, Paid, Cancelled.
- Abstrakte Syntaxbäume (AST) für Parser.
- API-Antworten: Ok, NotFound, Error.
- Ereignisse im System: UserLoggedIn, UserLoggedOut, UserRegistered.
6. Typische Fehler beim Arbeiten mit Sealed-Klassen
Fehler Nr. 1: Nicht alle Unterklassen in permits aufgeführt.
Wenn Sie nicht alle benötigten Unterklassen via permits auflisten, beschwert sich der Compiler sofort. Wenn Sie zum Beispiel permits Circle, Rectangle schreiben, aber Square vergessen und eine solche Klasse existiert, erhalten Sie einen Fehler.
Fehler Nr. 2: Die Unterklasse ist nicht final, sealed oder non-sealed.
Wenn die Unterklasse nicht mit dem erforderlichen Modifikator deklariert ist, meldet der Compiler einen Fehler: „Class must be either final, sealed or non-sealed“.
Fehler Nr. 3: Die Unterklasse ist in einer anderen Datei deklariert und für den Compiler nicht sichtbar.
Alle in permits aufgeführten Klassen müssen für den Compiler zugänglich sein – entweder in derselben Datei oder im selben Paket.
Fehler Nr. 4: Nicht alle Varianten im switch behandelt.
Wenn Sie eine Sealed-Klasse in einem switch mit Pattern Matching verwenden und eine Variante nicht behandeln, lässt der Compiler den Build nicht zu. Das ist gut – so verpassen Sie keinen Fall.
Fehler Nr. 5: Versuch, von einer Sealed-Klasse zu erben, die nicht in permits steht.
Wenn Sie versuchen, eine Unterklasse zu erstellen, die nicht in permits aufgeführt ist, erhalten Sie einen Fehler: „is not allowed to extend sealed class“.
Fehler Nr. 6: Versuch, Sealed-Klassen mit alten JDK-Versionen zu verwenden.
Sealed-Klassen gibt es erst seit Java 17. Wenn Sie versuchen, sie in einer älteren Version zu verwenden, erhalten Sie einen Compilerfehler oder können das Projekt überhaupt nicht bauen.
GO TO FULL VERSION