GoByte Skills Episode 5 of 27, track Java (2 of 3)

Sealed types: the switch that cannot forget

GoByte Skills #5: Seal the hierarchy, drop the default, and adding a type turns every switch that missed it into a compile error with a line number.

Piere is one of GoByte's characters. This post was drafted by AI agents in Piere's voice, then fact checked, run and edited by the GoByte team.

Back to top

sealed ... permits closes a hierarchy, and records are final. The compiler then knows every case, so a switch with no default is checked for completeness.

Animation for GoByte Skills #5: A new case appears and every switch that missed it lights up.
Transcript

A sealed Shape with Circle and Square feeds three switch cards that all pass; then Tri drops into permits and each switch that has no Tri case lights orange as the compiler error appears.

public class Main {
  static double area(Shape s) {
    return switch (s) { // no default
      case Circle c -> Math.PI * c.r() * c.r();
      case Square q -> q.side() * q.side();
    };
  }

  public static void main(String[] args) {
    System.out.println(area(new Square(3)));
  }
}

sealed interface Shape permits Circle, Square {}
record Circle(double r) implements Shape {}
record Square(double side) implements Shape {}
9.0

Now add Tri to permits and declare record Tri(double b, double h) implements Shape {}. Without touching area, the build fails at its switch: "the switch expression does not cover all possible input values". Every switch that forgot the newcomer is now a compile error with a line number.

Why it works#

permits is written into the class file (the PermittedSubclasses attribute) and enforced by the JVM: a class compiled elsewhere that claims to implement Shape fails to load with IncompatibleClassChangeError. Records are implicitly final, so nothing hides below them. With a finite, known set of types, the compiler checks a pattern switch the way it checks an enum switch expression: one case per permitted type, or no build.

There is a default anyway#

Javac adds a hidden one that throws. Recompile Shape with Tri but not area, call area(new Tri(1, 2)), and you get java.lang.MatchException instead of a silent wrong answer. Separate compilation is the only way to reach it.

Where it breaks#

Write default -> and the check is off for good, including for cases that do not exist yet. A non-sealed subtype reopens the hierarchy below it. And an if (s instanceof Circle c) chain gets no exhaustiveness check at all; only switch does.

Version notes#

Records arrived in Java 16, sealed types in 17, pattern matching for switch in 21 (JEP 441). The snippet needs 21, run as java Main.java.

Rule of thumb#

Seal the hierarchy, give every switch one case per type, and leave default out, so adding a type is a to-do list the compiler writes for you. A default branch on a sealed type is a smoke detector with the battery removed.

Report a mistake

Your product here? Partner with us

Back to top

Discussion

No comments yet. Signed in GoByte members with a verified e-mail can join. Community guidelines

Reading is open to everyone. Commenting and voting need a GoByte account with a verified e-mail.