Sealed Classes, Records, and Exhaustive Pattern Matching in switch
- Last updated
- Prerequisites:
- Java classes, interfaces, and inheritance
- Understanding of switch statements
- java
- sealed-classes
- records
- pattern-matching
- switch
- java-21
Read at your depth
The practical view
sealed classes restrict which classes can extend/implement them: public sealed interface Shape permits Circle, Square, Triangle {}. The permitted subclasses must be in the same module/package (or declared nested). Combined with records (data carriers), you get algebraic-data-type style modeling. Pattern matching for switch lets you write: return switch (shape) { case Circle c -> area(c); case Square s -> area(s); case Triangle t -> area(t); };. The compiler checks exhaustiveness: if you cover all permitted types, no default is needed; if you miss one (or add a new permitted type), the code doesn't compile.
The same idea in other frameworks
N/A — JVM language feature.N/A — JVM language feature.N/A — JVM language feature.N/A — JVM language feature.Legacy vs modern
instanceof chains vs exhaustive sealed switch
Hand-rolled instanceof chains are unverifiable and need a default; pattern matching on a sealed hierarchy lets the compiler prove completeness.
if (s instanceof Circle c) return area(c);
else if (s instanceof Square sq) return area(sq); // ❌ OLD: no exhaustiveness checkreturn switch (s) {
case Circle c -> Math.PI * c.r() * c.r();
case Square sq -> sq.side() * sq.side();
case Triangle t -> 0.5 * t.base() * t.height();
}; // ✅ NEW: compiler-verified exhaustiveInterview gotchas
Context
This tests the language-model understanding — interviewers want the closed-hierarchy property and how the compiler uses the permits list for totality, not just the syntax.
The mechanical answer
A sealed class/interfaces declares a closed set of permitted direct subclasses (via the permits clause, or nested declaration). The compiler enforces that only those types can extend it (across the same module), and the JVM verifies it at runtime through the PermittedSubclasses attribute. Because the hierarchy is closed and known statically, the compiler can enumerate every possible runtime type of a sealed value, so a switch over it can be proven exhaustive: cover all permitted types and no default is needed; miss one, or add a new permitted type, and the code fails to compile. This turns 'forgot a case' runtime bugs into compile-time errors and enables the compiler to generate efficient dispatch without a catch-all.
Trap
The naive answer is 'sealed just restricts inheritance'. The value is the closed-world totality proof it enables for pattern matching. Another trap: assuming permits is optional sugar — a sealed type without an explicit permits clause uses nested-declaration inference, but the rule that subclasses must be co-located (same package or module) still applies, and adding a subclass outside the permits list is a compile error. Candidates who connect sealing to exhaustiveness show real understanding.
Context
This probes the destructuring mechanics — interviewers want to know how case Point(int x, int y) extracts components and how nested patterns lower to bytecode.
The mechanical answer
A record pattern (case Point(int x, int y)) matches the selector against the record type and destructures its components: the compiler lowers it to an instanceof Point check, a cast, then reads the record's accessor methods (x() and y()), binding the results to pattern variables x and y. Nested patterns (case Line(Point a, Point b)) lower recursively: instanceof Line, then a destructured into a nested Point pattern check. The compiler also handles null safely (a pattern switch throws NPE if the selector is null, or you can add a case null). Type patterns and record patterns compose, and the exhaustiveness analysis treats record patterns as covering their record type when all components have patterns. The generated bytecode uses standard instanceof/cast chains plus the accessor invocations — no magic new bytecode.
Trap
The naive answer is 'record patterns are magic destructuring'. They compile to instanceof + accessor calls. Another trap: forgetting that pattern variables are scoped to the case block and that a guarded pattern (case Point p when p.x() > 0) re-tests the guard — a guard that fails does NOT make the case match, and exhaustiveness must account for unguarded fallthrough. Candidates who describe the lowering and guard semantics show real depth.