Comprehensive Guide to Record Patterns in Java

Target Java versions:
- Preview: Java 19 (JEP 405), Java 20 (JEP 432), Java 21 (JEP 440)
- Final / Standard: Java 21 (JEP 440 – Record Patterns), alongside JEP 441 (Pattern Matching for switch)

Record patterns extend Java’s pattern matching so you can deconstruct record values directly in switch, instanceof, and other pattern contexts.


1. What Are Record Patterns?

A record pattern lets you match a value that is a record and bind its components to variables in one step.

Given a record:

record Point(int x, int y) {}

Instead of:

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

You can write:

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

Here:

  • Point(int x, int y) is a record pattern.
  • It:
  • Tests that obj is a Point.
  • Deconstructs it into its components.
  • Binds x and y to the component values.

2. Prerequisites: Records + Pattern Matching

Record patterns combine two features:

  1. Records – special immutable data classes with named components.
  2. Pattern Matching – for instanceof and switch.

Record patterns depend on records:

  • They only work with record types.
  • They deconstruct record components in declared order.

3. Basic Syntax

General form:

TypeName(componentPattern1, componentPattern2, ...)

Examples:

record Point(int x, int y) {}

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

Each component in the pattern can itself be:

  • A type + variable pattern: int x
  • A nested pattern (another record pattern)
  • A var binding: var x
  • An underscore _ (if you use unnamed patterns, newer preview)

4. Record Patterns with instanceof

4.1. Simple Record Deconstruction

Object o = new Point(10, 20);

if (o instanceof Point(int x, int y)) {
    System.out.println("Point: " + x + ", " + y);
}

Semantics:

  • o instanceof Point(int x, int y) checks:
  • o != null
  • o is a Point
  • If true:
  • Binds x to ((Point)o).x()
  • Binds y to ((Point)o).y()

4.2. Combining with && Guards

if (o instanceof Point(int x, int y) && x == y) {
    System.out.println("On diagonal: " + x);
}

The pattern must match and the guard must be true for the if body to run.


4.3. Scope of Pattern Variables

if (o instanceof Point(int x, int y)) {
    // x and y are in scope here
}
// x and y are NOT in scope here

Same flow-analysis rules as other patterns:

  • Variables are only in scope where the pattern is guaranteed to have matched.

5. Record Patterns in switch

This is where they really shine.

5.1. Switch Expression Example

record Point(int x, int y) {}

String describe(Object obj) {
    return switch (obj) {
        case Point(int x, int y) -> "Point(" + x + ", " + y + ")";
        case null                -> "null";
        default                  -> "Unknown: " + obj;
    };
}
  • The pattern Point(int x, int y) both tests and binds.
  • No need to call p.x() / p.y() manually.

5.2. Multiple Record Cases

record Range(int start, int end) {}
record NamedPoint(String name, Point point) {}

String describe(Object obj) {
    return switch (obj) {
        case Point(int x, int y)                 -> "Point " + x + "," + y;
        case Range(int start, int end)          -> "Range[" + start + "," + end + "]";
        case NamedPoint(String n, Point p)      -> "Named " + n + " at " + p;
        case null                               -> "null";
        default                                 -> "Other: " + obj;
    };
}

6. Nested Record Patterns

Record patterns can be nested, deconstructing deep structures in one go.

6.1. Example

record Point(int x, int y) {}
record Line(Point from, Point to) {}

void printLine(Object o) {
    switch (o) {
        case Line(Point(int x1, int y1), Point(int x2, int y2)) ->
            System.out.println("Line from (" + x1 + "," + y1 + ") to (" + x2 + "," + y2 + ")");
        default ->
            System.out.println("Not a line");
    }
}

In Line(Point(int x1, int y1), Point(int x2, int y2)):

  • The outer pattern matches a Line.
  • The inner patterns match the from and to Points.
  • Then each Point is deconstructed into x and y.

6.2. Nested with Guards

switch (obj) {
    case Line(Point(int x1, int y1), Point(int x2, int y2))
            when x1 == x2 && y1 == y2 -> System.out.println("Degenerate line (point)");
    case Line(Point(int x1, int y1), Point(int x2, int y2)) ->
            System.out.println("Line from (" + x1 + "," + y1 + ") to (" + x2 + "," + y2 + ")");
    default ->
            System.out.println("Not a line");
}

7. Using var in Record Patterns

You can use var in record components if you don’t want to spell out the type again.

record Box<T>(T value) {}

switch (obj) {
    case Box(var v) -> System.out.println("Boxed: " + v);
    default         -> System.out.println("Not a box");
}

Or mixed:

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

switch (p) {
    case Pair(String s, var second) -> System.out.println("String + " + second);
    case Pair(var first, var second) -> System.out.println(first + " & " + second);
    default -> System.out.println("Not a pair");
}

Rules:

  • var lets the compiler infer the type from the record’s component type.
  • You can mix explicit types and var in the same record pattern.

8. Exhaustiveness and Sealed Types

Record patterns combine nicely with sealed types to allow exhaustive switches without default.

8.1. Algebraic Data Types (ADTs) Style

sealed interface Expr permits Num, Add, Mul {}

record Num(int value) implements Expr {}
record Add(Expr left, Expr right) implements Expr {}
record Mul(Expr left, Expr right) implements Expr {}

int eval(Expr expr) {
    return switch (expr) {
        case Num(int value) -> value;
        case Add(Expr l, Expr r) -> eval(l) + eval(r);
        case Mul(Expr l, Expr r) -> eval(l) * eval(r);
    }; // no default needed: sealed hierarchy exhaustive
}
  • The compiler knows all subtypes of Expr and all record components.
  • The switch is exhaustive — if you add a new subtype, you’ll get a compile error until you update the switch.

9. Unnamed Variables and Patterns (Preview Feature)

In newer previews (Java 22+), you can use underscores _ for parts you don’t care about.

Example (conceptual):

record Point(int x, int y) {}

switch (p) {
    case Point(int x, _) -> System.out.println("x only: " + x);
}
  • _ means “I don’t care about this component; don’t bind it.”

This is especially useful with large records.


10. Scope & Flow Analysis with Record Patterns

Same general rules as other patterns:

10.1. Scope inside switch cases

switch (obj) {
    case Point(int x, int y) -> {
        // x and y in scope here
    }
    default -> {
        // x and y NOT in scope here
    }
}

10.2. Scope with instanceof

if (o instanceof Point(int x, int y)) {
    // x, y in scope
}
// x, y not visible here

10.3. Early Returns and Refinement

void handle(Expr expr) {
    if (!(expr instanceof Num(int value))) {
        System.out.println("Not a number");
        return;
    }

    // Here, the compiler knows the only way we get here is if expr matched Num(int value)
    // so 'value' is in scope
    System.out.println("Number: " + value);
}

11. Limitations and Edge Cases

11.1. Only for Record Types

Record patterns only work on types that are actual records.

class Person { String name; int age; }

// ❌ Not allowed
if (obj instanceof Person(String name, int age)) { ... }

You must refactor Person to:

record Person(String name, int age) {}

11.2. Component Order Matters

Patterns match components in the declared order of the record:

record User(String name, int age) {}

switch (u) {
    case User(String name, int age) -> ...   // ✅
    case User(int age, String name) -> ...   // ❌ invalid order / types
}

You can’t reorder or change types in the pattern.


11.3. No Optional / Named Components

Patterns do not support “skipping” components by name:

record User(String name, int age, String email) {}

// ❌ Not allowed: you must match all components (unless using `_`)
case User(String name, int age) -> ...       // missing email

// ✅ But you can ignore via var/_:
case User(String name, int age, var ignored) -> ...
case User(String name, int age, _)          -> ...

(The _ version depends on unnamed patterns support.)


11.4. Deep Nesting Can Hurt Readability

Deeply nested record patterns can become hard to read:

case Tree(Node(Left(Leaf(int v1))), Node(Right(Leaf(int v2)))) -> ...

Use them wisely and refactor when patterns become too complex.


11.5. Mutability of Contained Objects

Record patterns deconstruct fields — they don’t magically enforce deep immutability:

record User(String name, List<String> roles) {}

if (obj instanceof User(String name, List<String> roles)) {
    // roles list may still be mutable
    roles.add("ADMIN");   // still allowed
}

12. Style Guidelines & Best Practices

12.1. Use Record Patterns for Data-Carrying Structures

Great for:

  • DTOs
  • Value objects
  • Expression trees
  • Configuration objects
  • API responses (when modeled as records)

12.2. Combine with Sealed Types for ADTs

Example: building algebraic-style data types for expression evaluation, ASTs, etc.

sealed interface Expr permits Num, Add, Mul, Neg {}

record Num(int value) implements Expr {}
record Add(Expr left, Expr right) implements Expr {}
record Neg(Expr value) 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 Neg(Expr v)         -> -eval(v);
    };
}

12.3. Avoid Overly Complex Patterns

If a pattern is very long or deeply nested, consider:

  • Introducing helper methods
  • Breaking it into multiple switches or if-statements
  • Using intermediate variables

12.4. Use var when Types Are Obvious

case Point(var x, var y) -> ...

But don’t overuse var where explicit types aid readability.


13. Typical Interview Questions (With Short Answers)

Q1. What is a record pattern?

A: A pattern that matches a record type and deconstructs it into its component values, binding them to variables in one step.


Q2. In which Java version were record patterns finalized?

A: Java 21 (JEP 440).


Q3. Where can record patterns be used?

A: Anywhere patterns are allowed:

  • instanceof tests
  • switch expressions/statements
  • Nested inside other patterns (e.g., record patterns, type patterns)

Q4. Can you use record patterns on non-record classes?

A: No. They only work with record types.


Q5. Do record patterns respect the declared order of components?

A: Yes. Pattern components must follow the same order and compatible types as in the record declaration.


Q6. How do record patterns interact with sealed types?

A: They enable exhaustive switches over sealed hierarchies by matching and deconstructing each record subtype.


A: Yes. Record patterns plus pattern matching for switch form a powerful, expression-oriented way to branch on structured data.


14. Summary

Record patterns are a major step toward declarative, expression-oriented programming in Java:

  • They let you deconstruct record values directly in pattern contexts.
  • They work with instanceof and switch.
  • They compose beautifully with:
  • Records
  • Sealed classes
  • Pattern matching for switch
  • They reduce boilerplate and make intent clear:
    “Match this shape and bind its parts.”

From Java 21 onward, record patterns are an essential tool for idiomatic modern Java, especially in domains involving structured data, ASTs, and algebraic-style models.

This guide covers their syntax, semantics, limitations, and interview angles so you shouldn’t need additional references for record patterns.