JavaSealed Classes

Sealed Classes

A sealed class (a standard feature since Java 17) restricts exactly which other classes or interfaces are allowed to extend or implement it. Instead of any class in your codebase being free to extend a public class, a sealed class names, up front, the complete, closed list of permitted subtypes.

A sealed hierarchy

Java
sealed interface Shape permits Circle, Square, Triangle {}

final class Circle implements Shape {
    double radius;
    Circle(double radius) { this.radius = radius; }
}

final class Square implements Shape {
    double side;
    Square(double side) { this.side = side; }
}

final class Triangle implements Shape {
    double base, height;
    Triangle(double base, double height) {
        this.base = base;
        this.height = height;
    }
}

Shape explicitly lists its only allowed implementers with permits Circle, Square, Triangle. No other class — anywhere, in any package — is permitted to implement Shape. The compiler enforces this list at compile time.

The permits clause

permits lists every class or interface allowed to directly extend or implement the sealed type. If every permitted subtype is declared in the same file as the sealed type, you can actually omit permits entirely — the compiler infers it from the file's contents. Once listed (or inferred), that list is closed: attempting to add a new subclass anywhere else in the codebase is a compile-time error.

Subclasses must declare their own openness

Every class permitted by a sealed type must itself pick exactly one of three modifiers, so there's never any ambiguity about what happens further down the hierarchy:

Modifier

Meaning

final

Closes the hierarchy here — cannot be extended further

sealed

Continues to restrict its own subtypes with its own permits list

non-sealed

Reopens the hierarchy — any class is free to extend this one

In the example above, Circle, Square, and Triangle are all declared final, so the Shape hierarchy is completely closed after one level — there can never be a fourth kind of Shape.

Why this matters: exhaustive pattern matching

Because the compiler knows the complete set of possible subtypes of a sealed type, a switch expression over that type can be checked for exhaustiveness — the compiler can confirm every case is covered, with no default branch needed at all.

An exhaustive switch — no default needed

Java
double area(Shape shape) {
    return switch (shape) {
        case Circle c -> Math.PI * c.radius * c.radius;
        case Square s -> s.side * s.side;
        case Triangle t -> 0.5 * t.base * t.height;
        // no 'default' required — the compiler knows these are ALL the Shapes
    };
}
Note
If you later add a new permitted subtype to `Shape`, every exhaustive `switch` like this one across your codebase will fail to compile until you add a matching case — the compiler catches missed cases for you automatically. See the dedicated **Pattern Matching** page for more on `switch` with type patterns like `case Circle c ->`.
  • sealed restricts which classes/interfaces may extend or implement a type.

  • permits names the closed, complete list of direct subtypes.

  • Every permitted subtype must be final, sealed, or non-sealed.

  • A closed hierarchy enables the compiler to verify switch exhaustiveness with no default case.