Record Patterns in Java

Naveen AnamaneniUpdated · 8 min read

A record pattern tests whether a value is an instance of a record and takes it apart into its components in the same step. Writing o instanceof Point(int x, int y) checks that o is a Point and declares x and y holding its components, with no calls to p.x() and p.y(). Record patterns can be nested, so one pattern can look several levels deep into a data structure. They became standard in Java 21 (JEP 440). In this lesson, you will learn the syntax, nesting, var, generics, how record patterns work in switch, and how they affect exhaustiveness.

Syntax

Syntax
RecordName(Pattern1, Pattern2, ...)

Each inner pattern matches one component, in the order the components are declared. An inner pattern can be:

Inner patternExampleMeaning
A type patternint x, String nameBind the component to a variable
varvar xBind it, letting the compiler infer the type
Another record patternPoint(int x, int y)Take the component apart too
The unnamed pattern_Ignore the component (see "Unnamed Variables and Patterns")

Example: Nested Patterns, Var and Generics

Let's see an example that takes apart a Line made of two Points, and then a generic Box:

Shapes.java▶ Try it
public class Shapes {
    record Point(int x, int y) { }
    record Line(Point start, Point end) { }
    record Box<T>(T content) { }
 
    public static void main(String[] args) {
        Object o = new Line(new Point(0, 0), new Point(3, 4));
        if (o instanceof Line(Point(var x1, var y1), Point(int x2, int y2))) {
            System.out.println("dx=" + (x2 - x1) + " dy=" + (y2 - y1));
        }
 
        Box<String> b = new Box<>("Navika");
        if (b instanceof Box(var s)) {
            System.out.println(s.toUpperCase());
        }
    }
}
Output
dx=3 dy=4
NAVIKA

Explanation:

  • Nesting: Line(Point(...), Point(...)) checks that o is a Line, that both components are Points, and binds all four coordinates in one condition.
  • var: var x1 takes the component's declared type, int. You can mix var and explicit types freely.
  • Generics: Box(var s) doesn't repeat <String>. The compiler infers Box<String> from the type of b, so s is a String and s.toUpperCase() compiles.
One pattern takes the whole structure apart

How Components Are Read

A record pattern gets each component by calling the record's accessor method. If you override an accessor, the pattern uses your version:

Accessor.java▶ Try it
public class Accessor {
    record Student(String name, int marks) {
        public String name() {
            System.out.println("  name() called");
            return name;
        }
    }
 
    public static void main(String[] args) {
        Object o = new Student("Advitha", 88);
        if (o instanceof Student(String n, int m)) {
            System.out.println(n + " " + m);
        }
    }
}
Output
  name() called
Advitha 88

Explanation: The pattern called name() to fill n. That's why record patterns work only with records: a record's components and accessors are known exactly, while an ordinary class has no such guarantee. Using a record pattern on a normal class is an error: "deconstruction patterns can only be applied to records, P is not a record".

Record Patterns in Switch

Record patterns are most useful in switch. Here is the calculator from "Exhaustive Switch with Sealed Types", rewritten so each case takes its record apart directly, plus a simplify operation that looks inside nested records:

Calc2.java▶ Try it
public class Calc2 {
    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 { }
 
    static 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);
        };
    }
 
    static Expr simplify(Expr e) {
        return switch (e) {
            case Mul(Num(int v), Expr r) when v == 0 -> new Num(0);
            case Mul(Expr l, Num(int v)) when v == 0 -> new Num(0);
            case Mul(Num(int v), Expr r) when v == 1 -> simplify(r);
            case Add(Num(int v), Expr r) when v == 0 -> simplify(r);
            case Add(Expr l, Expr r) -> new Add(simplify(l), simplify(r));
            case Mul(Expr l, Expr r) -> new Mul(simplify(l), simplify(r));
            case Num n -> n;
        };
    }
 
    public static void main(String[] args) {
        Expr e = new Add(new Num(0), new Mul(new Num(1), new Num(7)));
        System.out.println(e);
        System.out.println(simplify(e));
        System.out.println(eval(e));
    }
}
Output
Add[left=Num[value=0], right=Mul[left=Num[value=1], right=Num[value=7]]]
Num[value=7]
7

Explanation: 0 + (1 * 7) is simplified in two steps. Add(Num(int v), Expr r) when v == 0 matches an addition whose left side is the number 0, and returns the simplified right side. Then Mul(Num(int v), Expr r) when v == 1 matches 1 * 7 and returns 7. Without record patterns, each rule would need several instanceof checks and accessor calls. The last three unguarded cases make the switch exhaustive.

Exhaustiveness with Nested Patterns

The compiler checks exhaustiveness through nested patterns too. With sealed interface Pet permits Cat, Dog and record Pair(Pet first, Pet second), all four combinations must be covered:

Pets.java▶ Try it
public class Pets {
    sealed interface Pet permits Cat, Dog { }
    record Cat(int age) implements Pet { }
    record Dog(int age) implements Pet { }
    record Pair(Pet first, Pet second) { }
 
    static String pair(Pair p) {
        return switch (p) {
            case Pair(Cat a, Cat b) -> "two cats";
            case Pair(Dog a, Dog b) -> "two dogs";
            case Pair(Cat a, Dog b) -> "cat then dog";
            case Pair(Dog a, Cat b) -> "dog then cat";
        };
    }
 
    public static void main(String[] args) {
        System.out.println(pair(new Pair(new Dog(3), new Cat(2))));
        System.out.println(pair(new Pair(new Cat(2), new Cat(5))));
    }
}
Output
dog then cat
two cats

Leave out the last case and the compiler notices that Pair(Dog, Cat) isn't handled:

Compiler output
R2.java:7: error: the switch expression does not cover all possible input values
        return switch (p) {
               ^
1 error

The Rules

RuleError if broken
The pattern must list every component, in order"incorrect number of nested patterns"
Only records can be deconstructed"deconstruction patterns can only be applied to records"
Each nested type must be possible for that component"incompatible types"
Nested type patterns match null only if they are total(see "Handling Null in Switch")

For record Point(int x, int y), writing Point(int x) fails:

Compiler output
R3.java:5: error: incorrect number of nested patterns
        if (o instanceof Point(int x)) { }
                         ^
  required: int,int
  found: int
1 error

Common Mistakes

Leaving out components. Every component needs a pattern; use _ for the ones you don't need.

Getting the order wrong. Components match by position, not by name; Point(int y, int x) binds y to the x-coordinate.

Expensive or side-effecting accessors. Patterns call accessors, so keep them simple. If one throws, the switch throws MatchException.

Trying to deconstruct a normal class. Only records support record patterns.

Interview Corner

What are record patterns?

Record patterns (Java 21) test whether a value is an instance of a record and extract its components into variables in one step, for example if (o instanceof Point(int x, int y)). They can be nested (Line(Point(var x1, var y1), Point p2)), use var, infer generic type arguments, and appear in switch cases with guards. Components are read through the accessor methods. The compiler checks exhaustiveness through nested patterns, so a switch over records with sealed component types can omit default.

Practice

  1. Easy: Write record Rectangle(Point topLeft, Point bottomRight) and compute its area with a single nested record pattern.
  2. Harder: Add a rule to simplify that turns Mul(x, Num(1)) into simplify(x). Test it on (3 + 0) * 1.

Quick check: Record patterns

4 questions
1What does o instanceof Point(int x, int y) do?
2How does a record pattern read components?
3Why does Point(int x) fail for record Point(int x, int y)?
4Can record patterns be used with ordinary classes?

Key Takeaways

Was this tutorial helpful?

Lesson 460 of 482