Java Sealed Classes Cheat Sheet
Syntax for sealed, non-sealed, and final permitted subclasses, plus exhaustive pattern matching with switch, as finalized in modern Java.
Declaring a Sealed Hierarchy
sealed restricts which classes/interfaces may extend or implement it.
public sealed interface Shape permits Circle, Square, Rectangle {}public final class Circle implements Shape { public final double radius; public Circle(double radius) { this.radius = radius; }}public final class Square implements Shape { public final double side; public Square(double side) { this.side = side; }}// Permitted subclasses must be final, sealed, or non-sealedpublic non-sealed class Rectangle implements Shape { public double width, height;}
Same-File / Same-Module Shortcut
permits is optional when all subclasses are in the same file.
// If all permitted subtypes live in the same source file, permits can be omittedsealed interface Result<T> { record Ok<T>(T value) implements Result<T> {} record Err<T>(String message) implements Result<T> {}}
Exhaustive Pattern Matching
The compiler verifies switch covers every permitted subtype — no default needed.
static double area(Shape shape) { return switch (shape) { case Circle c -> Math.PI * c.radius * c.radius; case Square s -> s.side * s.side; case Rectangle r -> r.width * r.height; // no default clause required: compiler proves exhaustiveness };}// With record patterns (deconstruction)static String describe(Result<Integer> result) { return switch (result) { case Result.Ok<Integer> ok -> "value: " + ok.value(); case Result.Err<Integer> err -> "error: " + err.message(); };}
Permitted-Subclass Modifiers
Every direct subclass of a sealed type must pick exactly one.
- final- no further subclassing allowed
- sealed- continues the restriction with its own permits list
- non-sealed- reopens the hierarchy, any class may extend it
- permits- explicit list of allowed direct subtypes (optional if same file)
- record- records are implicitly final, so they satisfy the sealed contract directly
Reflecting Over Sealed Types
Class exposes isSealed() and getPermittedSubclasses() for generic tooling.
public sealed interface Shape permits Circle, Square, Rectangle {}// java.lang.Class exposes sealed metadata via reflectionClass<?> clazz = Shape.class;boolean sealed = clazz.isSealed(); // trueClass<?>[] permitted = clazz.getPermittedSubclasses();for (Class<?> c : permitted) { System.out.println(c.getName());}// Useful for building generic serializers/validators that must stay// in sync with the hierarchy without hardcoding subtype lists.// A non-sealed class reports isSealed() == false even if its supertype is sealedSystem.out.println(Rectangle.class.isSealed()); // false
Guarded & Nested Record Patterns
The 'when' clause adds boolean conditions to a case label without breaking exhaustiveness.
sealed interface Result<T> permits Result.Ok, Result.Err { record Ok<T>(T value) implements Result<T> {} record Err<T>(String message, int code) implements Result<T> {}}static String describe(Result<Integer> result) { return switch (result) { // Guarded pattern: 'when' adds a boolean condition after the match case Result.Ok<Integer> ok when ok.value() > 100 -> "big win: " + ok.value(); case Result.Ok<Integer> ok -> "ok: " + ok.value(); // Nested record pattern: deconstruct straight into the record's components case Result.Err<Integer>(String msg, int code) when code >= 500 -> "server error: " + msg; case Result.Err<Integer>(String msg, int code) -> "client error " + code + ": " + msg; };}// If a 'when' guard doesn't match, the compiler falls through to the next label// in source order -- guarded patterns do NOT count toward exhaustiveness on their own.
Cross-Package Sealed Hierarchies
permits accepts fully-qualified names, but every subtype must share the sealed type's module.
// permits accepts fully-qualified names across packages, but every permitted// subtype must be co-located with the sealed type: same module (named modules)// or same package (unnamed/classpath scenarios).package com.example.shapes;public sealed interface Shape permits com.example.shapes.circles.Circle, com.example.shapes.polygons.Square {}// module-info.java for a named-module project must still expose both packagesmodule com.example.app { exports com.example.shapes; exports com.example.shapes.circles; exports com.example.shapes.polygons;}// Compile error if a permitted class lives in an unrelated module:// "class Circle is not allowed to extend sealed class Shape: module// com.example.app does not contain it"
Sealed Types With Generics
Exhaustiveness is checked on the raw permitted type, not on the type argument.
sealed interface Container<T> permits Box, Empty {}record Box<T>(T value) implements Container<T> {}record Empty<T>() implements Container<T> {}// Pattern matching on a generic sealed type only checks the raw/permitted type,// not the type argument -- the compiler can't prove exhaustiveness across T.static <T> String show(Container<T> c) { return switch (c) { case Box<T> b -> "box: " + b.value(); case Empty<T> e -> "empty"; // exhaustive over Container's permitted subtypes regardless of T };}// Beware: you cannot add a type-argument-specific permits list (e.g. permits// Box<String>) -- sealing is enforced on the raw type; generics are erased// for this purpose just like everywhere else in the JVM.
Compiler Rules & Edge Cases
Lesser-known constraints the compiler enforces on sealed hierarchies.
- Same compilation unit- permits is optional only if every direct subtype is declared in the same source file
- Module/package co-location- permitted subtypes must share the sealed type's module (named modules) or package (unnamed)
- No local/anonymous subtypes- a sealed class/interface cannot be directly implemented by a local class, anonymous class, or lambda
- Every direct subtype self-declares- final, sealed, or non-sealed is mandatory on each permitted subclass, no default
- Interfaces can be sealed too- permits works identically whether classes implement or interfaces extend the sealed type
- Records are implicitly final- a record listed in permits automatically satisfies the final/sealed/non-sealed rule
- sealed does not imply abstract- a sealed class can still be instantiated directly unless it is also declared abstract
Combine sealed hierarchies with exhaustive switch expressions to get compile-time enforcement that every case is handled — adding a new permitted subtype breaks the build everywhere a switch forgot to handle it, which is exactly what you want.