Contents

A sealed class uses the sealed modifier and a permits clause listing its allowed subclasses. Each permitted subclass must itself be declared as one of: final (no further subclassing), sealed (extends the sealed hierarchy), or non-sealed (reopens the hierarchy for arbitrary extension).

// Sealed class with three permitted subclasses public sealed class Shape permits Circle, Rectangle, Triangle {} // final — no further subclassing allowed public final class Circle extends Shape { private final double radius; public Circle(double radius) { this.radius = radius; } public double radius() { return radius; } } // final subclass public final class Rectangle extends Shape { private final double width, height; public Rectangle(double width, double height) { this.width = width; this.height = height; } public double width() { return width; } public double height() { return height; } } // non-sealed — allows anyone to extend Triangle public non-sealed class Triangle extends Shape { private final double base, height; public Triangle(double base, double height) { this.base = base; this.height = height; } public double base() { return base; } public double height() { return height; } } // Anyone can now extend Triangle (but NOT Shape directly) public class EquilateralTriangle extends Triangle { public EquilateralTriangle(double side) { super(side, side * Math.sqrt(3) / 2); } } Sealed classes and their permitted subclasses must be in the same package (or module). If in the same source file, the permits clause can be omitted — the compiler infers it.

Sealed interfaces work the same way. Permitted implementors can be final classes, sealed classes, non-sealed classes, or even records (records are implicitly final).

public sealed interface Expr permits Num, Add, Mul, Neg, Var {} public record Num(double value) implements Expr {} public record Add(Expr left, Expr right) implements Expr {} public record Mul(Expr left, Expr right) implements Expr {} public record Neg(Expr expr) implements Expr {} public record Var(String name) implements Expr {} // Sealed interface with mixed implementors public sealed interface Result<T> permits Result.Ok, Result.Err { record Ok<T>(T value) implements Result<T> {} record Err<T>(String msg) implements Result<T> {} static <T> Result<T> ok(T value) { return new Ok<>(value); } static <T> Result<T> err(String msg) { return new Err<>(msg); } }

The biggest advantage of sealed types is that the compiler knows the complete set of subtypes at compile time. A pattern-matching switch over a sealed type is checked for exhaustiveness — if you miss a case, you get a compile-time error, not a runtime exception.

static double area(Shape shape) { // No default needed — compiler verifies all cases are covered return switch (shape) { case Circle c -> Math.PI * c.radius() * c.radius(); case Rectangle r -> r.width() * r.height(); case Triangle t -> 0.5 * t.base() * t.height(); // Triangle is non-sealed, so its subtypes don't add new cases here }; } // Recursive expression evaluator — no default case needed static double eval(Expr expr, Map<String, Double> env) { return switch (expr) { case Num(var v) -> v; case Var(var name) -> env.getOrDefault(name, 0.0); case Neg(var e) -> -eval(e, env); case Add(var l, var r) -> eval(l, env) + eval(r, env); case Mul(var l, var r) -> eval(l, env) * eval(r, env); }; } // Usage Map<String, Double> env = Map.of("x", 3.0, "y", 4.0); Expr expr = new Add(new Mul(new Var("x"), new Var("x")), new Mul(new Var("y"), new Var("y"))); System.out.println(eval(expr, env)); // 25.0 (x²+y²) If you add a new permitted subtype to a sealed hierarchy, every exhaustive switch over that type will immediately fail to compile — forcing you to handle the new case explicitly. This is the key safety guarantee.

Sealed types shine for modeling domain concepts that have a fixed set of variants — similar to algebraic data types (sum types) in functional languages.

// Payment method hierarchy — every variant is known public sealed interface PaymentMethod permits CreditCard, BankTransfer, Crypto {} public record CreditCard(String number, String holder, int cvv) implements PaymentMethod {} public record BankTransfer(String iban, String bic) implements PaymentMethod {} public record Crypto(String walletAddress, String coin) implements PaymentMethod {} // Processing logic — exhaustive, no instanceof chains static String processPayment(PaymentMethod method, double amount) { return switch (method) { case CreditCard(var num, var holder, var cvv) -> "Charging %.2f to card ending %s for %s" .formatted(amount, num.substring(num.length() - 4), holder); case BankTransfer(var iban, var bic) -> "Transferring %.2f to IBAN %s (BIC: %s)" .formatted(amount, iban, bic); case Crypto(var wallet, var coin) -> "Sending %.2f worth of %s to %s" .formatted(amount, coin, wallet); }; }
// Enums work naturally as sealed-type implementors public sealed interface Status permits Status.Active, Status.Inactive, Status.Pending { enum Active implements Status { INSTANCE } enum Inactive implements Status { INSTANCE } enum Pending implements Status { INSTANCE } } // Or use a real enum — it is implicitly sealed public enum OrderStatus { PLACED, CONFIRMED, SHIPPED, DELIVERED, CANCELLED } // Exhaustive switch over enum: static String label(OrderStatus s) { return switch (s) { case PLACED -> "Order placed"; case CONFIRMED -> "Confirmed"; case SHIPPED -> "On the way"; case DELIVERED -> "Delivered"; case CANCELLED -> "Cancelled"; }; }