🚀 Modern Java 17 → 25 · Advanced

Sealed hierarchies + exhaustive switch in Java

Algebraic data types with no default branch needed.

🧩 The mysteryYou add a new Triangle shape and the build breaks in 6 places. Annoying? No: the compiler just found 6 bugs before any user could.

A closed list of subtypes

A sealed interface or class controls exactly which types may extend it: sealed interface Shape permits Circle, Square {}. Nobody else can implement Shape, so the compiler knows the complete list of possibilities.

Every subtype declares its openness

Each direct subtype must be **final (no further subclasses), sealed (only its listed subclasses) or non-sealed** (re-opened: anyone may extend it). Records and enums are implicitly final, so they qualify automatically.

sealed interface V permits Car, Bus, Bike {}
final class Car implements V {}
sealed class Bus implements V
    permits MiniBus {}
final class MiniBus extends Bus {}
non-sealed class Bike implements V {}

Exhaustive, no default

Because the compiler knows every subtype, a switch that handles them all is exhaustive: no default branch needed.

sealed interface Shape permits Circle, Square {}
record Circle(double r) implements Shape {}
record Square(double s) implements Shape {}
 
double area(Shape sh) {
    return switch (sh) {
        case Circle(var r) -> Math.PI * r * r;
        case Square(var s) -> s * s;
    };
}
🔮 Predict it

Your turn

No default branch. What does this print?

sealed interface Light permits Red, Green {}
record Red() implements Light {}
record Green(int secs) implements Light {}
void main() {
    Light l = new Green(30);
    System.out.println(switch (l) {
        case Red r -> "stop";
        case Green g -> "go " + g.secs();
    });
}
  1. go 30
  2. Compile error: missing default
  3. stop
Show the answer

go 30. Light is sealed to Red and Green, both are handled, so the switch is exhaustive without a default. The value is a Green with 30 seconds.

⚠️ The trap

Missing a case? It won't compile

Leave out a permitted subtype and the switch isn't exhaustive: compile error, even if the value at runtime happens to be a type you did handle. And beware default: it silences this check, so a new subtype slips into it quietly. With sealed types, prefer no default.

// Pet permits Dog, Cat
Pet p = new Dog();
String s = switch (p) {
    case Dog d -> "dog";
};  // compile error: Cat not covered
🤔 Think first

Six broken builds

Your team adds record Triangle(...) implements Shape. The build now fails in 6 places. Why is that good news?

Think about it, then reveal the answer

Each failure is a switch over Shape that is no longer exhaustive. The compiler hands you the exact list of places that must handle Triangle, turning a forgotten case into a compile error instead of a production bug.

💼 In the real world

Closed domains

Payment states, API responses, shapes in a drawing app, commands in a protocol: whenever the set of variants is known, seal it. Library authors also seal types to control who may extend their API, without making everything final.

Key takeaways

  1. sealed ... permits A, B names the allowed subtypes
  2. Subtypes must be final, sealed, or non-sealed (records are final)
  3. A switch covering every subtype needs no default
  4. New subtype → incomplete switches fail to compile (good!)
🤯 Did you know?

The JDK seals its own types: java.lang.constant.ConstantDesc is a sealed interface whose permitted subtypes include String, Integer and ClassDesc.

Practice questions

What does this print?

sealed interface Pet permits Dog, Cat {}
record Dog(String name) implements Pet {}
record Cat(int lives) implements Pet {}
void main() {
    Pet p = new Cat(9);
    System.out.println(switch (p) {
        case Dog d -> "woof " + d.name();
        case Cat c -> "meow x" + c.lives();
    });
}
  1. meow x9
  2. Compile error: missing default
  3. woof null
  4. Throws MatchException
Check your answer

meow x9. Pet is sealed to Dog and Cat, and both are handled, so the switch is exhaustive without a default.

What does this print?

sealed interface Pet permits Dog, Cat {}
record Dog() implements Pet {}
record Cat() implements Pet {}
void main() {
    Pet p = new Dog();
    String s = switch (p) {
        case Dog d -> "dog";
    };
    System.out.println(s);
}
  1. Compile error
  2. dog
  3. Throws MatchException
  4. null
Check your answer

Compile error. Cat is a permitted Pet with no matching case, so the switch isn't exhaustive. The compiler refuses it even though p happens to be a Dog.

Next: say "I don't care" in code. The underscore _ for variables and pattern parts you'll never use.