☕ Java

Record Patterns

Record patterns, finalized in Java 21 (JEP 440) alongside pattern matching for switch, extend type patterns specifically for records: rather than only confirming a value is an instance of a record type and binding the whole record to a single variable, a record pattern destructures the record's components directly within the pattern itself, binding each component to its own variable in the same step as the type test — and because a record's component can itself be another record, record patterns nest arbitrarily, letting deeply nested data be fully destructured in a single, visually tree-shaped pattern. This entry covers exactly what separate accessor calls record patterns eliminate, nested destructuring syntax and how it mirrors the data's own shape, using var for component types versus explicit types, and record patterns used directly in instanceof versus within switch's pattern-matching cases.

Motivation — Eliminating Accessor Calls After a Type Match

Pattern matching for instanceof (Java 16) let a type check and a cast happen in one step, but for a record specifically, confirming a value's type with obj instanceof SomeRecord r still left every individual component reachable only through its own accessor call (r.x(), r.y(), and so on) — the type match bound the whole record to one variable, but getting at the data inside it still required a separate method call per component, immediately after the pattern match, every single time. For records with several components, or records nested inside other records (a common, natural shape for records to take, given how well-suited they are to representing structured data), this meant a successful type-and-destructure operation conceptually happening in the programmer's head ("this is a Point with these x,y values") still required multiple separate accessor calls in the actual code to realize that same operation. Java 21's record patterns (JEP 440) close this gap by letting the pattern itself describe the record's component structure directly: instanceof obj instanceof Point(int x, int y) doesn't just confirm obj is a Point and bind the whole record to a variable — it simultaneously binds x and y, the record's own two components, to two correctly-typed local variables, in the exact same step as the type test, with no separate accessor calls needed at all. The pattern's shape — the type name followed by parentheses listing each component's own (possibly further nested) pattern — visually mirrors the actual structure of the data being matched, which becomes especially valuable once nesting is involved.
Java
// ── Before record patterns — type match, then separate accessor calls ──
record Point(int x, int y) {}

void printOld(Object obj) {
    if (obj instanceof Point p) {        // type-only match — whole record bound to "p"
        int x = p.x();                    // separate accessor call #1
        int y = p.y();                    // separate accessor call #2
        System.out.println("x=" + x + ", y=" + y);
    }
}

// ── Java 21 — record pattern destructures components in the SAME step ──
void printNew(Object obj) {
    if (obj instanceof Point(int x, int y)) {   // type test AND destructuring, one step
        System.out.println("x=" + x + ", y=" + y);   // x, y already bound — no accessor calls
    }
}

Nested Destructuring — Patterns That Mirror the Data's Shape

Because a record's component can itself be another record type, a record pattern's own component positions can themselves be record patterns rather than plain type-and-bind patterns — and this nesting can go arbitrarily deep, matching whatever actual nesting depth the underlying data has. The result is that a single pattern expression can destructure straight through several levels of record nesting in one step, with the pattern's own visual structure (parentheses nested inside parentheses) directly mirroring the nested structure of the data being matched, which makes a reasonably complex nested-data destructuring operation read as a single, recognizable shape rather than as a sequence of progressively-deeper accessor chains (event.location().coordinates().x()) that a reader has to mentally assemble back into the nested structure they actually represent. This is particularly valuable for tree-shaped or hierarchically nested domain data — geometric shapes built from points, events with locations with coordinates, AST/expression-tree nodes built from other nodes — exactly the kind of data records are naturally well-suited to representing in the first place, since records are immutable, transparent carriers for fixed sets of components, and component values that are themselves records compose naturally into trees. Record patterns don't require destructuring every level fully, either — a pattern can choose to destructure some nested components while leaving others bound as a whole record (mixing a nested record pattern at one component position with a plain type pattern, or even just a type name with no further destructuring, at another), giving the pattern author full control over exactly how deep the destructuring goes at each position independently.
Java
// ── Nested records — naturally tree-shaped domain data ─────────────────
record Point(int x, int y) {}
record Location(String city, Point coordinates) {}
record Event(String name, Location location) {}

// ── Fully nested record pattern — destructures THROUGH every level ─────
void describe(Object obj) {
    if (obj instanceof Event(String name, Location(String city, Point(int x, int y)))) {
        // name, city, x, y ALL bound here — three levels of nesting destructured in one pattern,
        // mirroring the actual Event -> Location -> Point structure visually
        System.out.println(name + " in " + city + " at (" + x + "," + y + ")");
    }
}

// Contrast — the pre-record-pattern equivalent, accessor chains reconstructing the same nesting:
void describeOld(Object obj) {
    if (obj instanceof Event e) {
        String name = e.name();
        String city = e.location().city();
        int x = e.location().coordinates().x();
        int y = e.location().coordinates().y();
        System.out.println(name + " in " + city + " at (" + x + "," + y + ")");
    }
}

// ── Partial destructuring — mix nested patterns and plain bindings freely
void describePartial(Object obj) {
    if (obj instanceof Event(String name, Location loc)) {
        // "loc" left as a whole Location here — NOT further destructured at this position,
        // while "name" IS pulled out directly — author controls depth per-component
        System.out.println(name + " happened in " + loc.city());
    }
}

var in Record Patterns, and Usage in instanceof vs switch

Each component position within a record pattern can use var instead of writing out the component's declared type explicitly, letting the compiler infer that position's type from the record's own declaration exactly as var does for ordinary local variable declarations — a useful option for reducing visual noise in patterns with several components or several levels of nesting, particularly when the component types are already obvious from the record's own name and context, though (mirroring the general style guidance for var on ordinary local variables) explicit types remain preferable wherever they add real, non-obvious clarity for a reader, such as distinguishing between similarly-shaped records with meaningfully different component types. Record patterns work identically whether used in an instanceof expression or within a switch's case labels — the destructuring syntax and semantics are exactly the same in both contexts, since switch's pattern matching is built on the same underlying pattern-matching machinery instanceof uses. The practical difference is purely structural: instanceof with a record pattern is a single conditional test, appropriate for a one-off "if this specific shape matches, do this" check, while switch with record patterns across multiple case labels is appropriate for dispatching across several different record shapes (or different sealed-hierarchy variants, each represented by its own record), gaining switch's exhaustiveness checking (covered separately) when the selector's type is sealed or an enum — record patterns and pattern matching for switch are designed to compose directly, with the combination of the two being one of the more common and powerful uses of modern Java's pattern matching.
Java
// ── var within a record pattern — inferred per-component ───────────────
record Point(int x, int y) {}

void printVar(Object obj) {
    if (obj instanceof Point(var x, var y)) {   // both inferred as int — still fully type-checked
        System.out.println(x + y);
    }
}

// Mixing var and explicit types within the same pattern is also allowed:
record Measurement(String label, double value) {}
void printMixed(Object obj) {
    if (obj instanceof Measurement(String label, var value)) {
        System.out.println(label + ": " + value);
    }
}

// ── Same record pattern syntax/semantics in instanceof vs switch ───────
sealed interface Shape permits Circle, Rectangle {}
record Circle(Point center, double radius) implements Shape {}
record Rectangle(Point topLeft, Point bottomRight) implements Shape {}

// instanceof — single, one-off conditional test:
void checkOne(Shape shape) {
    if (shape instanceof Circle(Point(var cx, var cy), var radius)) {
        System.out.println("Circle at (" + cx + "," + cy + "), r=" + radius);
    }
}

// switch — dispatching across multiple record shapes, WITH exhaustiveness:
double area(Shape shape) {
    return switch (shape) {
        case Circle(Point(var cx, var cy), var radius) -> Math.PI * radius * radius;
        case Rectangle(Point(var x1, var y1), Point(var x2, var y2)) ->
            Math.abs((x2 - x1) * (y2 - y1));
        // no default — Circle/Rectangle is Shape's complete permitted set,
        // compiler-verified exhaustive, exactly as with non-destructuring sealed switches
    };
}

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.