Sealed classes and interfaces (since Java 17) restrict which classes or interfaces may extend or implement them. This enables exhaustive pattern matching and clearer domain modeling.

Sealed Class

  public sealed class Shape
        permits Circle, Rectangle, Triangle {
    // common behavior
}

public final class Circle extends Shape {
    private final double radius;
    public Circle(double radius) { this.radius = radius; }
    public double radius() { return radius; }
}

public final class Rectangle extends Shape {
    private final double width, height;
    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }
}

public non-sealed class Triangle extends Shape {
    // non-sealed: further subclasses are allowed
}
  

Permitted Subclass Modifiers

Every permitted subclass must be one of:

Modifier Meaning
final Cannot be extended further
sealed Must declare its own permits clause
non-sealed Open for further extension

Sealed Interfaces

  public sealed interface Payment
        permits CreditCard, BankTransfer, Crypto { }

public record CreditCard(String number, String cvv) implements Payment { }
public record BankTransfer(String iban) implements Payment { }
public record Crypto(String walletAddress) implements Payment { }
  

Pattern Matching with Sealed Types

Sealed hierarchies enable exhaustive switch:

  public double area(Shape shape) {
    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();
    }; // No default needed — compiler verifies exhaustiveness
}
  

If a new subclass is added to the sealed hierarchy, the compiler flags any non-exhaustive switch.

Sealed Classes vs Final vs Abstract

Feature final abstract sealed
Can be extended No Yes (by anyone) Yes (only permitted types)
Can be instantiated Yes No Depends on subclass
Use case Prevent extension Define template Controlled hierarchy

Real-World Use Cases

  • AST nodes in compilers or interpreters
  • Result types: Success | Failure | Pending
  • Domain events with known variants
  • API response types with fixed shapes
  public sealed interface Result<T> permits Success, Failure {
    record Success<T>(T value) implements Result<T> { }
    record Failure<T>(String error) implements Result<T> { }
}
  

Best Practices

  • Use sealed types when the set of variants is fixed and known
  • Combine with records for concise variant definitions
  • Prefer final permitted subclasses unless further extension is intentional
  • Use exhaustive switch instead of instanceof chains when possible