☕ Java

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, a class or interface author had only two real options for controlling subtyping: declare the type final, which forbids any subtyping whatsoever, or leave it open (the default), which permits literally any class anywhere — in the same codebase, in a separate library, in code the original author will never see — to extend or implement it, with absolutely no way to enumerate, limit, or even discover the complete set of subtypes that might exist at any given time. This all-or-nothing choice was a poor fit for a very common and useful design intent: a type hierarchy that is deliberately meant to have multiple variants (so final is wrong), but where that set of variants is meant to be fixed and fully known by the type's own author, with no possibility of an unknown third party introducing an unanticipated additional variant (so ordinary open extension is also wrong). This intent shows up constantly in domain modeling — a Shape that is exactly Circle, Square, or Triangle and nothing else; an Expression in an interpreter that is exactly a fixed set of node types; a Result that is exactly Success or Failure — where the entire point of the design is that the set of variants is closed and complete, and code working with the type (especially a switch statement dispatching on which variant it has) should be able to rely on that completeness, both for the human reader's understanding and, ideally, for the compiler to actually verify that every variant has been handled, rather than silently doing nothing for some category of value the original code's author never anticipated. Without sealed types, the closest approximation was a comment or documentation simply asserting "don't extend this outside this package," which the compiler does nothing to enforce, leaving the closed-hierarchy intent unverified and easily violated by anyone who simply ignores the comment. Sealed classes and interfaces, introduced via JEP 409 and finalized in Java 17, make this design intent a first-class, compiler-enforced language construct: the sealed modifier plus a permits clause declares the type's complete, fixed list of permitted direct subtypes, checked and enforced by the compiler, not merely documented.
Java
// ── 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 documentation

The permits Clause and Required Subtype Modifiers

A sealed class or interface declares its permitted direct subtypes with permits ClassA, ClassB, ClassC after the class/interface header; if every permitted subtype is declared in the very same source file as the sealed type itself, the permits clause can actually be omitted entirely, since the compiler can simply scan that file to discover them — the explicit permits clause is required only when the permitted subtypes are spread across multiple files (which is the far more common real-world case, since each subtype typically warrants its own file). A meaningful compiler-enforced rule is that every class or interface named in a permits clause must be directly accessible to the sealed type at compile time, and — critically — must be located either in the same module (if the sealed type's module is named/explicit) or, for an unnamed module, in the same package, as the sealed type itself. This is what makes a sealed hierarchy genuinely closed in a way a mere "permits" comment never could be: it's not just that the compiler checks the listed names — it's that the permitted subtypes are themselves required to live within a boundary (module or package) that the sealed type's own author controls, meaning no external module or differently-packaged code, even if it somehow knew the exact class name to use, could ever satisfy the compiler's accessibility requirement to legitimately extend the sealed type from outside that boundary. Every class explicitly permitted by a sealed type must itself declare exactly one of three modifiers, and the compiler requires one of them to be present — there is no implicit default, forcing each permitted subtype's own further extensibility to be an explicit decision rather than an oversight. final closes that branch of the hierarchy completely — no further subtyping below it at all. sealed continues the closed-hierarchy pattern one level deeper, with its own permits clause declaring its own permitted subtypes. non-sealed deliberately reopens that one specific branch back to ordinary, unrestricted open inheritance, which is useful when one particular variant of an otherwise-closed hierarchy is intentionally meant to be open for arbitrary further extension by anyone, while the rest of the hierarchy remains closed.
Java
// ── 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-sealed

Sealed Hierarchies with Records and Switch Exhaustiveness

Sealed types and records are an especially natural pairing, because records already express exactly the kind of fixed-shape, fully-known data variant that a sealed type's permitted subtypes very often are — a sealed interface listing several record implementations directly expresses "this is one of exactly these N fixed data shapes," with records supplying the data-carrying half of that expression and sealed supplying the closed-set half. Because records are implicitly final, a record implementing a sealed interface automatically satisfies the required final/sealed/non-sealed modifier rule without needing to write final explicitly (records are final by their own nature, as covered separately) — though it is also valid and common to write it explicitly for clarity. The most significant practical payoff of sealed types, beyond pure design clarity, is exhaustiveness checking under switch expressions and pattern matching for switch: because a sealed type's set of permitted direct subtypes is a complete, compiler-known list, a switch statement or expression with a case for every one of those permitted subtypes is recognized by the compiler as exhaustive, exactly the same fail-fast safety net enum types have always provided — no default case is required, and adding a new permitted subtype later without updating every switch over that sealed type causes those switches to fail to compile, surfacing the omission immediately at the next build rather than as a silent runtime gap. This compiler-verified exhaustiveness over a sealed hierarchy is what makes the otherwise-error-prone "I added a new case to my closed set of variants, did I remember to update every place that switches on it?" question something the compiler answers definitively, rather than something that depends on the original author (or, much more likely, someone else entirely, possibly years later) remembering to search the codebase for every relevant switch — a guarantee that simply isn't available for an ordinary, non-sealed, open type hierarchy, since the compiler has no way to know the complete set of subtypes that could ever exist for an open type.
Java
// ── 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 {}

Related Topics in Java 9 to 21 Features

Module System (JPMS)
The Java Platform Module System (JPMS), introduced in Java 9 under Project Jigsaw, adds a module as a new, higher-level unit of code organization above the package, allowing a JAR-like artifact to explicitly declare which packages it exports for use by other modules, which modules it depends on, and which packages it keeps entirely internal and inaccessible from outside — closing a long-standing gap in the classpath model where every public class in every JAR on the classpath was accessible to every other JAR, regardless of whether that access was intended. JPMS was also the mechanism used to decompose the JDK itself, which had grown into one monolithic rt.jar, into roughly 90 separate modules, enabling smaller custom runtime images and explicit internal-API encapsulation (notably restricting access to sun.* and other internal packages that many libraries had relied on despite warnings). This entry covers the classpath problems JPMS solves, module-info.java syntax and the requires/exports/opens directives, the distinction between the classpath and module path including automatic and unnamed modules, and jlink custom runtime images.
JShell
JShell, introduced in Java 9, is an interactive Read-Eval-Print Loop (REPL) bundled directly with the JDK, allowing Java statements, expressions, method, class, and variable declarations to be entered and evaluated one at a time at an interactive prompt, with immediate feedback — without requiring a class wrapper, a main method, explicit compilation, or a build tool, all of which Java had always required for even the smallest snippet of executable code before Java 9. This solved a long-standing friction point for learning, quick experimentation, and API exploration, where languages like Python or Ruby offered an immediate interactive prompt but Java required the full edit-compile-run cycle even to test a single line. This entry covers what problem JShell solves and how it differs from a script, core REPL usage including implicit variable creation and forward references, snippet management commands, and how to feed JShell external classes and files.
Collection Factory Methods
Collection factory methods — List.of(...), Set.of(...), and Map.of(...)/Map.ofEntries(...) — introduced in Java 9, provide a concise, standard way to create small, fixed-content, genuinely immutable collections in a single expression, replacing the previously common but verbose combination of Arrays.asList(...), Collections.unmodifiableList(...), and manual add() calls, none of which produced a collection that was both concise to write and fully immutable. This entry covers exactly what problem these factories solve compared to the older idioms, the specific immutability and null-rejection guarantees they provide (which are stricter than Collections.unmodifiableXxx), their behavior around duplicate keys/elements, and important caveats around mutability expectations and performance for very small collections.
Private Interface Methods
Private interface methods, introduced in Java 9, allow an interface to declare methods marked private — with a full method body — that are accessible only from within that same interface (from its default methods, its other private methods, and its private static methods), but completely invisible to implementing classes and to any code outside the interface. They close a code-duplication gap left after Java 8's default methods: when an interface has multiple default methods that share common logic, that shared logic previously either had to be duplicated in each default method or exposed as a public default method purely to enable reuse, polluting the interface's actual API surface with a method meant only for internal reuse. This entry covers the specific problem they solve, the distinction between private instance and private static interface methods, the rules governing what they can call and be called from, and how they interact with the broader default-method design Java established in version 8.