Sealed Classes and Pattern Matching — Specification Reading Guide¶
Sealed types and pattern matching are language features — every word of behaviour is fixed by the JLS, recorded in the class file by the JVMS, and traceable through a chain of JEPs from Java 15 (first preview) to Java 21 (record patterns, final pattern-match switch). This file maps each language-level guarantee —
permits,non-sealed,final/sealed/non-sealedtriad, type patterns, record patterns, exhaustiveness — to the section of the JLS or JVMS that binds it, and to the JEP that proposed it.
1. Where to find the canonical text¶
| Concept | Authoritative source |
|---|---|
sealed/non-sealed/final triad on classes | JLS §8.1.1.2 — sealed Classes |
sealed/non-sealed/final triad on interfaces | JLS §9.1.1.4 — sealed Interfaces |
permits clause | JLS §8.1.6, §9.1.4 |
| Permitted-subclass placement rules | JLS §8.1.6, §9.1.4 (same module, same package or unnamed module) |
| Switch statements and expressions | JLS §14.11 — The switch Statement; §15.28 — Switch Expressions |
Switch labels (case, default, patterns) | JLS §14.11.1 |
| Exhaustiveness for switch | JLS §14.11.1.2 |
| Patterns (general) | JLS §14.30 — Patterns |
| Type patterns | JLS §14.30.2 |
| Record patterns | JLS §14.30.5 |
Pattern matching in instanceof | JLS §15.20.2 |
PermittedSubclasses class-file attribute | JVMS §4.7.31 |
ACC_SEALED, ACC_FINAL, ACC_NON_SEALED | JVMS §4.1 (access flags on ClassFile) |
invokedynamic | JVMS §6.5.invokedynamic |
SwitchBootstraps.typeSwitch | java.lang.runtime.SwitchBootstraps (JDK API) |
MatchException | java.lang.MatchException (Java 19+) |
The JLS binds javac; the JVMS binds the JVM; the JEPs trace how the feature arrived at its current shape.
2. JEP timeline¶
| JEP | Feature | Java release | Status |
|---|---|---|---|
| 360 | Sealed classes (preview) | Java 15 (Sept 2020) | Preview 1 |
| 397 | Sealed classes (second preview) | Java 16 (Mar 2021) | Preview 2 |
| 409 | Sealed classes (final) | Java 17 (Sept 2021) | Final |
| 305 | Pattern matching for instanceof (preview) | Java 14 (Mar 2020) | Preview 1 |
| 375 | Pattern matching for instanceof (second preview) | Java 15 | Preview 2 |
| 394 | Pattern matching for instanceof (final) | Java 16 (Mar 2021) | Final |
| 406 | Pattern matching for switch (preview) | Java 17 | Preview 1 |
| 420 | Pattern matching for switch (second preview) | Java 18 | Preview 2 |
| 427 | Pattern matching for switch (third preview) | Java 19 | Preview 3 |
| 433 | Pattern matching for switch (fourth preview) | Java 20 | Preview 4 |
| 441 | Pattern matching for switch (final) | Java 21 (Sept 2023) | Final |
| 405 | Record patterns (preview) | Java 19 | Preview 1 |
| 432 | Record patterns (second preview) | Java 20 | Preview 2 |
| 440 | Record patterns (final) | Java 21 (Sept 2023) | Final |
The reading order: 360 → 397 → 409 for sealing; 394 for instanceof patterns; 406 → 441 for switch patterns; 432 → 440 for record patterns. Each JEP is a focused 5–15 page document. Reading the final JEP and the previous preview gives you the rationale plus the design evolution.
3. JLS §8.1.1.2 — sealed classes¶
§8.1.1.2 (paraphrased): A class declaration with the
sealedmodifier introduces a sealed class. A sealed class has an associated set of permitted direct subclasses. Every direct subclass of a sealed class must be declaredfinal,sealed, ornon-sealed.
Three rules bind any sealed class:
- Closure — only the classes listed in
permits(or, if omitted, those declared in the same compilation unit) may directly extend the sealed class. - Modifier requirement — every direct subclass must be
final,sealed, ornon-sealed. No subclass may omit all three; the compiler rejects the declaration. - Module/package proximity — every named permitted subclass must be accessible to the sealed class and live in the same module (named module) or in the same package within the unnamed module.
public sealed class Vehicle permits Car, Truck, Motorcycle {}
public final class Car extends Vehicle {}
public final class Truck extends Vehicle {}
public non-sealed class Motorcycle extends Vehicle {} // re-opens
If you omit permits, the compiler infers it from the same compilation unit:
public sealed class Op { ... } // permits inferred
final class Add extends Op { ... }
final class Sub extends Op { ... }
The inferred list is still recorded in PermittedSubclasses (JVMS §4.7.31), so reflection still sees the closure.
4. JLS §9.1.1.4 — sealed interfaces¶
§9.1.1.4 (paraphrased): An interface declaration with the
sealedmodifier introduces a sealed interface. Each permitted direct subclass or subinterface must befinal,sealed, ornon-sealed. Records implementing the interface are implicitlyfinal.
The rules mirror sealed classes, with two interface-specific points:
- Records as implementers. Records are implicitly
final(JLS §8.10), so a record may implement a sealed interface without declaring any sealed/final/non-sealed modifier on itself. This is the dominant idiom for ADTs. extendsfor sub-interfaces. Asealedinterface may be extended by anothersealedornon-sealedinterface; the same triad applies.
public sealed interface Shape permits Circle, Square, Triangle {}
public record Circle(double r) implements Shape {}
public record Square(double s) implements Shape {}
public record Triangle(double b, double h) implements Shape {}
Circle, Square, Triangle are implicitly final because they're records. No explicit modifier is needed on them.
5. JLS §8.1.6 / §9.1.4 — permitted-subclass placement¶
The permits clause may name a type T as a permitted direct subclass of S only if:
- T is accessible to S (visibility rules of §6.6 apply), and
- T and S are members of the same module, if either is in a named module, or
- T and S are members of the same package in the unnamed module.
That is: cross-module sealing is forbidden. The rationale is concrete — if a sealed type's permits could span modules, the compiler could not verify the closure without loading the entire module graph at every compilation, and downstream modules could force themselves into a closed set.
// File: app/Shape.java
package app;
public sealed interface Shape permits app.geom.Circle, app.geom.Square {}
// File: app/geom/Circle.java
package app.geom;
public record Circle(double r) implements app.Shape {} // OK — same module
If app is module M1 and app.geom is module M2, the declaration fails to compile with:
6. JVMS §4.7.31 — PermittedSubclasses class-file attribute¶
The closure is recorded at the bytecode level by a class-file attribute named PermittedSubclasses. JVMS §4.7.31 defines its layout:
PermittedSubclasses_attribute {
u2 attribute_name_index; // → "PermittedSubclasses"
u4 attribute_length; // = 2 + 2 * number_of_classes
u2 number_of_classes;
u2 classes[number_of_classes]; // CONSTANT_Class_info indexes
}
The attribute appears in the parent's class file. Each entry is a constant-pool index of a permitted direct subclass.
You can inspect it with javap -v:
Two consequences worth knowing:
- Runtime closure. When the JVM loads a permitted subclass, it verifies the parent's
PermittedSubclassesactually contains it. A handcrafted.classfile that pretends to extend a sealed class fails verification — sealing is not just a compile-time check. - Reflection.
Class.getPermittedSubclasses()reads this attribute and returns the array of permittedClass<?>objects. The presence of the attribute is what makesClass.isSealed()returntrue.
The class file also carries access flags (ACC_FINAL, ACC_SEALED, ACC_NON_SEALED) on every class to indicate which of the three modifiers it carries. ACC_SEALED is 0x0001 in the JEP 409 set; final and non-sealed reuse pre-existing flags.
7. JLS §14.30 — patterns¶
§14.30 introduces patterns as a general construct. A pattern is a "match against a value": it succeeds or fails, and on success it may bind values to variables.
Three pattern kinds (Java 21):
- Type pattern (§14.30.2):
T name— matches if the scrutinee is an instance ofT, bindsnameto the scrutinee cast toT. - Record pattern (§14.30.5):
R(P1, P2, ...)where R is a record type and eachPiis itself a pattern — matches if the scrutinee is anRand each component matches the nested pattern. - Type pattern with
var(§14.30.2):var name— matches anything; the type ofnameis inferred from context.
// Type patterns
case Integer i -> ...
case String s -> ...
// Record patterns (single level)
case Point(int x, int y) -> ...
// Record patterns (nested)
case Line(Point(int x1, int y1), Point(int x2, int y2)) -> ...
// var in a record pattern
case Point(var x, var y) -> ...
The grammar allows arbitrary nesting. The compiler verifies the pattern shape matches the record's declared components (in declaration order). You cannot reorder or skip components in a record pattern; if you want to ignore one, use var _ (unnamed pattern, JEP 443/456, preview in Java 21/22, finalised in Java 22+).
8. JLS §14.11.1.2 — exhaustiveness¶
§14.11.1.2 defines when a switch is exhaustive. The compiler must prove that for every possible value of the scrutinee, at least one case label matches. For a sealed scrutinee T permits A, B, C, the static covering of {A, B, C} is required.
The exact rule, in plain English:
A set of case labels exhaustively covers a sealed type T if for every direct permitted subtype S of T, there is a case label that covers S — either explicitly (case S) or transitively (case U where U is a supertype of S that itself covers S).
Examples:
// T permits Mammal, Bird; Mammal permits Dog, Cat — both forms are exhaustive
case Dog d -> ...
case Cat c -> ...
case Bird b -> ...
// or:
case Mammal m -> ...
case Bird b -> ...
// Non-exhaustive — Triangle is missing
case Circle c -> ...
case Square s -> ...
// → compile error: not exhaustive
default always satisfies exhaustiveness. The compiler accepts it; you usually don't want it on a sealed scrutinee for the reasons in junior.md and senior.md.
For non-sealed scrutinee types (e.g., Object), exhaustiveness can only be reached via default. The compiler enforces total coverage either way — a switch expression that is not exhaustive fails to compile.
9. JLS §15.20.2 — instanceof patterns¶
§15.20.2 defines the instanceof pattern expression: e instanceof T t. The expression evaluates to true if e is an instance of T and not null; on true, t is bound to e cast to T.
The binding is flow-sensitive. JLS §6.3 defines the scope of pattern variables: they are in scope where the test is known to have succeeded. This includes the if body, the || short-circuit fall-through, and so on.
if (obj instanceof String s && !s.isEmpty()) { ... } // s in scope here
if (!(obj instanceof String s)) { return; }
useString(s); // s in scope here too
A common subtlety: if the scope leaks outside the test, the binding is not in scope at every reachable line. The compiler computes scope precisely per §6.3. See find-bug.md for a leak that compiles but surprises.
10. java.lang.runtime.SwitchBootstraps and MatchException¶
Pattern-match switch lowers to invokedynamic at the bytecode level (JVMS §6.5.invokedynamic). The bootstrap method is java.lang.runtime.SwitchBootstraps.typeSwitch:
public static CallSite typeSwitch(
MethodHandles.Lookup lookup,
String invocationName,
MethodType invocationType,
Object... labels);
The labels argument is the array of case shapes: Class<?> objects for type patterns, integer/string constants for value cases, and synthesized handles for record patterns. The returned CallSite exposes a method (Object scrutinee, int startIndex) -> int that returns the index of the first matching case.
For sealed scrutinee types, the compiler computes the case order so the bootstrap can answer in O(N) for N cases, and the JIT can specialize the lookup further. Subsequent invocations of the call site reuse the cached MethodHandle.
MatchException (Java 19+, java.lang.MatchException) is the runtime safety net. It is thrown when:
- A switch was compiled as exhaustive but at runtime no case matches (binary compatibility break — a new permit appeared).
- A guarded case (
whenclause) was the only candidate but its guard returnedfalseat runtime, and there is no other matching case.
MatchException is a RuntimeException. You don't catch it in production code; its appearance indicates either a binary-version mismatch or a guard-coverage bug.
11. Reading list¶
- JLS §8.1.1.2 — sealed classes; the modifier triad and
permits. - JLS §9.1.1.4 — sealed interfaces; identical machinery for interfaces.
- JLS §8.1.6, §9.1.4 — placement rules for permitted subclasses (module/package proximity).
- JLS §14.11 and §15.28 — switch statements and expressions;
caselabel syntax. - JLS §14.11.1.2 — exhaustiveness for sealed scrutinees.
- JLS §14.30 — patterns; sub-sections for type patterns (§14.30.2) and record patterns (§14.30.5).
- JLS §15.20.2 —
instanceofpattern expressions and binding scope (§6.3). - JVMS §4.7.31 —
PermittedSubclassesclass-file attribute. - JVMS §6.5.invokedynamic — the bytecode that lowers pattern-match switch.
- JEP 360, 397, 409 — sealed classes from preview to final.
- JEP 394 — pattern matching for
instanceof(final). - JEP 406, 441 — pattern matching for
switch(preview → final). - JEP 432, 440 — record patterns (preview → final).
java.lang.runtime.SwitchBootstraps— javadoc; the runtime bootstrap method.- Brian Goetz et al. — State of the Specialisation and State of the Pattern design documents on openjdk.org. Background reading for why the design is what it is.
- Barbara Liskov, John Guttag — Abstraction and Specification in Program Development (MIT Press, 1986) — origin of ADTs as a software-engineering concept.
The spec text is concise — sealed classes occupy under two pages of JLS §8, and the PermittedSubclasses attribute is half a page of JVMS §4.7.31. Read them once with javap -v in another terminal; they map onto each other one-to-one.
The summary: the JLS gives you sealed, non-sealed, permits, type patterns, record patterns, and exhaustiveness checking; the JVMS records closure in PermittedSubclasses (§4.7.31) and lowers pattern switches through invokedynamic; the JDK runtime provides SwitchBootstraps.typeSwitch and MatchException. JEPs 360/397/409 brought sealing in; JEP 394 brought pattern instanceof; JEPs 406/441 brought pattern switch; JEPs 432/440 brought record patterns. Every word of behaviour above traces to one of those documents — no implementation accidents, no hidden semantics.