Sealed Classes
Sealed classes and interfaces, finalized in Java 17 (JEP 409, after preview in Java 15–16), let a class or interface explicitly restrict, via a permits clause, exactly which other classes or interfaces are allowed to extend or implement it — giving a type hierarchy author fine-grained control between the two previous extremes of final (no subtyping permitted at all) and ordinary open inheritance (any class, anywhere, can extend or implement, with no way to enumerate or limit that set). This entry covers the closed-hierarchy problem sealed types solve and why exhaustiveness reasoning matters, the permits clause and the specific compiler-enforced rules about where permitted subtypes must live, the required modifiers permitted subtypes must use, and how sealed hierarchies combine with records and pattern matching for switch.
Motivation — Between final and Fully Open Inheritance
// ── Before Java 17 — closed-hierarchy intent only expressible as a comment
// "Do not extend this class outside this package" — a comment, NOT enforced:
public abstract class ShapeOld {
// intended to be ONLY Circle, Square, Triangle — but nothing stops
// any other class, anywhere, from also extending ShapeOld
}
public class UnexpectedShape extends ShapeOld {} // compiles fine — intent silently violated
// ── Java 17+ — sealed type, COMPILER-ENFORCED closed hierarchy ─────────
public sealed class Shape permits Circle, Square, Triangle {}
public final class Circle extends Shape {}
public final class Square extends Shape {}
public final class Triangle extends Shape {}
// public class UnexpectedShape extends Shape {}
// COMPILE ERROR: "class UnexpectedShape is not allowed to extend sealed class Shape:
// it must be listed in Shape's permits clause"
// The set {Circle, Square, Triangle} is now GUARANTEED complete — verified
// by the compiler, not merely asserted in documentationThe permits Clause and Required Subtype Modifiers
// ── permits clause omitted — compiler infers it from same-file subtypes ─
sealed interface Animal {}
record Dog() implements Animal {} // all permitted subtypes in the SAME file —
record Cat() implements Animal {} // permits clause not required, compiler scans the file
record Bird() implements Animal {}
// ── permits clause required when subtypes live in separate files ───────
// Shape.java:
public sealed interface Shape permits Circle, Square {}
// Circle.java:
public record Circle(double radius) implements Shape {}
// Square.java:
public record Square(double side) implements Shape {}
// Both Circle and Square must live in the same package/module as Shape —
// the compiler enforces this accessibility boundary, not just the name list
// ── Every permitted subtype MUST declare final, sealed, or non-sealed ──
public sealed class Vehicle permits Car, Truck, Motorcycle {}
public final class Car extends Vehicle {}
// "final" — Car's branch of the hierarchy is now completely closed,
// no further subtyping below it at all
public sealed class Truck extends Vehicle permits PickupTruck, BoxTruck {}
// "sealed" — Truck continues the closed-hierarchy pattern one level deeper,
// with its OWN permits clause for its own permitted subtypes
public final class PickupTruck extends Truck {}
public final class BoxTruck extends Truck {}
public non-sealed class Motorcycle extends Vehicle {}
// "non-sealed" — deliberately REOPENS this one branch; ANY class anywhere
// can now extend Motorcycle freely, exactly like ordinary open inheritance:
public class SportsMotorcycle extends Motorcycle {} // perfectly legal — no permits needed here
// Omitting all three modifiers on a permitted subtype is a COMPILE ERROR:
// public class Bus extends Vehicle {}
// error: class Bus does not specify final, sealed, or non-sealedSealed Hierarchies with Records and Switch Exhaustiveness
// ── Sealed interface + record implementations — the natural, common pairing
sealed interface Shape permits Circle, Square, Triangle {}
record Circle(double radius) implements Shape {} // implicitly final, as all records are
record Square(double side) implements Shape {}
record Triangle(double base, double height) implements Shape {}
// ── Compiler-verified exhaustiveness over the sealed hierarchy ─────────
double area(Shape shape) {
return switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Square s -> s.side() * s.side();
case Triangle t -> 0.5 * t.base() * t.height();
// NO default needed — compiler verifies Circle/Square/Triangle is
// Shape's COMPLETE permitted set, exactly like enum exhaustiveness
};
}
// ── Adding a new permitted subtype surfaces every now-incomplete switch ─
// Shape.java updated to:
// sealed interface Shape permits Circle, Square, Triangle, Pentagon {}
// record Pentagon(double side) implements Shape {}
//
// The "area" switch above now FAILS TO COMPILE:
// error: the switch expression does not cover all possible input values
// because this kind of Shape is not handled in the switch statement: 'Pentagon'
//
// This omission is caught immediately at build time — not discovered later
// as a silent runtime gap (e.g. area() returning 0 or throwing for Pentagon)
// ── Contrast — an OPEN (non-sealed) hierarchy offers no such guarantee ──
interface OpenShape {} // not sealed — anyone, anywhere, can implement this
double areaOpen(OpenShape shape) {
return switch (shape) {
case Circle2 c -> Math.PI * c.radius() * c.radius();
default -> throw new IllegalStateException("Unknown shape: " + shape);
// "default" is MANDATORY here — compiler has no way to know the
// complete set of OpenShape implementations that could ever exist
};
}
record Circle2(double radius) implements OpenShape {}