Skip to content

Java · Modern Java

Java Record Patterns — Inline Deconstruction for Cleaner Switch Dispatch

8 min read Updated 2026-06-20 Share:

Practice Record Patterns interview questions

What record patterns add to pattern matching

Record patterns (finalized in Java 21, JEP 440) extend Java's pattern matching to allow inline deconstruction of record components. Instead of testing a type, casting, and then calling accessors, you state the shape of the data you expect and bind each component to a variable in one expression.

Before record patterns:

Object obj = new Point(3, 4);

if (obj instanceof Point p) {
    int x = p.x(); // accessor
    int y = p.y(); // accessor
    System.out.println(x + ", " + y);
}

With record patterns (Java 21):

if (obj instanceof Point(int x, int y)) {
    System.out.println(x + ", " + y); // x and y bound directly
}

The type is checked and the components are destructured in a single expression. The pattern Point(int x, int y) reads like a constructor call in reverse — "match a Point and extract its x and y."

Record patterns in switch

Record patterns are most powerful in switch expressions over sealed types:

sealed interface Shape permits Circle, Rectangle, Triangle {}
record Circle(double radius)          implements Shape {}
record Rectangle(double w, double h)  implements Shape {}
record Triangle(double base, double h) implements Shape {}

double area(Shape s) {
    return switch (s) {
        case Circle(double r)              -> Math.PI * r * r;
        case Rectangle(double w, double h) -> w * h;
        case Triangle(double b, double h)  -> 0.5 * b * h;
    };
}

The compiler verifies exhaustiveness (all three Shape subtypes are covered), and the components are bound inline — no instanceof checks, no casts, no accessor chains.

Nested record patterns

Components that are themselves records can be destructured inline, eliminating deep accessor chains:

record Point(int x, int y) {}
record Line(Point start, Point end) {}

Object obj = new Line(new Point(0, 0), new Point(3, 4));

// Accessor chain — verbose:
if (obj instanceof Line l) {
    int x1 = l.start().x(), y1 = l.start().y();
    int x2 = l.end().x(),   y2 = l.end().y();
}

// Nested record pattern — concise:
if (obj instanceof Line(Point(int x1, int y1), Point(int x2, int y2))) {
    double length = Math.sqrt(Math.pow(x2-x1, 2) + Math.pow(y2-y1, 2));
    System.out.println("length: " + length); // 5.0
}

Nesting is arbitrarily deep. This is especially clean for recursive data structures:

sealed interface Expr permits Num, Add, Mul {}
record Num(int v)           implements Expr {}
record Add(Expr l, Expr r)  implements Expr {}
record Mul(Expr l, Expr r)  implements Expr {}

int eval(Expr e) {
    return switch (e) {
        case Num(int v)           -> v;
        case Add(Expr l, Expr r)  -> eval(l) + eval(r);
        case Mul(Expr l, Expr r)  -> eval(l) * eval(r);
    };
}

// eval(Mul(Num(2), Add(Num(3), Num(4)))) = 2 * (3 + 4) = 14

Using var to infer component types

var can be used in a record pattern to let the compiler infer the component type:

record Pair<A, B>(A first, B second) {}

Object obj = new Pair<>("hello", 42);

if (obj instanceof Pair(var first, var second)) {
    System.out.println(first + " / " + second);
    // first is Object (due to erasure), second is Object
}

// If you know the concrete types:
if (obj instanceof Pair(String first, Integer second)) {
    System.out.println(first.toUpperCase() + " " + (second * 2));
    // compiler may warn: unchecked cast due to erasure
}

For generic records, var avoids unchecked warnings. Use explicit types only when you're certain of the runtime types.

Guarded patterns — when clause

when guards attach a boolean condition to a record pattern:

String classify(Object obj) {
    return switch (obj) {
        case Point(int x, int y) when x == 0 && y == 0 -> "origin";
        case Point(int x, int y) when x == 0            -> "y-axis";
        case Point(int x, int y) when y == 0            -> "x-axis";
        case Point(int x, int y)                        -> "quadrant " + quadrant(x, y);
        default                                         -> "not a point";
    };
}

Cases are evaluated top-to-bottom; the when guard is checked only after the type and structure match. More specific guards must come before more general ones.

Scope and null behaviour

Variables bound in a record pattern follow the same flow-sensitive scope rules as all pattern variables:

  • In scope in the true branch and && chains.
  • Not in scope in the false/else branch or || chains.
  • Never bound for nullinstanceof returns false for null regardless of pattern.
Object obj = null;
System.out.println(obj instanceof Point(int x, int y)); // false — no NPE

// Handle null explicitly in switch:
switch (obj) {
    case null                    -> System.out.println("null");
    case Point(int x, int y)     -> System.out.println(x + ", " + y);
    default                      -> System.out.println("other");
}

Exhaustiveness and sealed types

Exhaustiveness checking covers which subtypes are handled, not which component values. Adding a new subtype to a sealed interface breaks every exhaustive switch that doesn't handle it — the compiler guides you to every site that needs updating:

sealed interface Event permits Login, Logout {}
record Login(String userId, Instant at)  implements Event {}
record Logout(String userId, Instant at) implements Event {}

void audit(Event e) {
    switch (e) {
        case Login(String id, Instant t)  -> log("login",  id, t);
        case Logout(String id, Instant t) -> log("logout", id, t);
    }
    // Adding 'PasswordChange' to Event → compile error here
}

Replacing the Visitor pattern

Record patterns with sealed types are the idiomatic modern replacement for the Visitor pattern. The Visitor existed to add operations on a closed type hierarchy without modifying each class — sealed + switch achieves the same without the ceremony:

// Visitor — requires: interface ExprVisitor<R>, accept() in each Expr, ExprVisitorImpl
// ~60 lines for a 3-node tree

// Record patterns — zero infrastructure:
int eval(Expr e) {
    return switch (e) {
        case Num(int v)           -> v;
        case Add(Expr l, Expr r)  -> eval(l) + eval(r);
        case Mul(Expr l, Expr r)  -> eval(l) * eval(r);
    };
}

String prettyPrint(Expr e) {
    return switch (e) {
        case Num(int v)           -> String.valueOf(v);
        case Add(Expr l, Expr r)  -> "(" + prettyPrint(l) + " + " + prettyPrint(r) + ")";
        case Mul(Expr l, Expr r)  -> "(" + prettyPrint(l) + " × " + prettyPrint(r) + ")";
    };
}

Adding a new operation (eval, prettyPrint, typecheck) means writing one new function. Adding a new type (e.g., Div) breaks every existing switch — the compiler tells you exactly where to add the new case.

When to use record patterns vs accessors

Use a record pattern when:

  • You need most or all components of a record.
  • You're dispatching over multiple record types in a switch.
  • Components are nested records and accessor chains would be verbose.

Use accessors when:

  • You only need one or two components of a large record.
  • The binding variable name from the outer instanceof is already clear.
// Only need x — accessor is simpler:
if (obj instanceof Point p) {
    process(p.x());
}

// Need both — record pattern wins:
if (obj instanceof Point(int x, int y)) {
    process(x, y);
}

Recap

Record patterns (Java 21) destructure record components inline in instanceof and switch, eliminating the accessor boilerplate when you need multiple values. They support nesting (components that are themselves records), var for type inference, and when guards for component-level conditions. Null never matches a record pattern — handle it with case null in switch. Exhaustiveness checking in switch covers which sealed subtypes are handled; component-value exhaustiveness uses when plus a catch-all case. Combined with sealed interfaces, record patterns replace the Visitor pattern: adding a new operation is a new function; adding a new type variant causes compile errors at every exhaustive switch that needs updating.

More ways to practice

The self-quiz is live. Get notified when mock interviews and new question packs drop.

or
Join our WhatsApp Channel