Java 21 Record Patterns

July 31, 20264 min readUpdated 8/20/2026

A record pattern matches a record and pulls its components out in the same step. It is a small piece of syntax that removes the accessor calls from every branch of a type switch, and it composes to arbitrary depth.

It only works on records, and that is not an arbitrary restriction. A record publishes its components as part of its contract — the compiler knows their order, types and accessors — so the destructuring is guaranteed to match the construction. An ordinary class has no such promise, which is why there is nothing to destructure.

The syntax

record Point(int x, int y) { }

class Demo {
    String before(Object value) {
        if (value instanceof Point p) {
            return "at " + p.x() + "," + p.y();      // bind the record, then call accessors
        }
        return "not a point";
    }

    String after(Object value) {
        if (value instanceof Point(int x, int y)) {  // bind the components directly
            return "at " + x + "," + y;
        }
        return "not a point";
    }
}

The pattern mirrors the record's declaration. Point(int x, int y) checks that the value is a Point and binds its two components to x and y.

Component names in the pattern are yours to choose — they need not match the record's:

record Point(int x, int y) { }

class Demo {
    void run(Object value) {
        if (value instanceof Point(int across, int down)) {
            System.out.println(across + down);
        }
        if (value instanceof Point(var a, var b)) {   // var works too
            System.out.println(a + b);
        }
    }
}

In a switch

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

class Geometry {
    double area(Shape shape) {
        return switch (shape) {
            case Circle(double r) -> Math.PI * r * r;
            case Rectangle(double w, double h) -> w * h;
            case Triangle(double b, double h) -> b * h / 2;
        };
    }
}

Compare that with the version that binds the record and calls accessors — case Circle c -> Math.PI * c.radius() * c.radius(). The pattern version puts the value you care about directly in scope, which is most of the readability win.

No default is needed because Shape is sealed; the compiler knows the three cases are all of them.

Nesting

The real power is that patterns compose. A record containing records can be destructured in one expression:

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

class Demo {
    String describe(Object value) {
        return switch (value) {
            // Reach two levels down in one pattern
            case Line(Point(var x1, var y1), Point(var x2, var y2)) when x1 == x2 ->
                    "vertical line at x=" + x1;
            case Line(Point(var x1, var y1), Point(var x2, var y2)) when y1 == y2 ->
                    "horizontal line at y=" + y1;
            case Line(Point start, Point end) ->
                    "line from " + start + " to " + end;
            default -> "not a line";
        };
    }
}

Note the mixed depth in the last case: a nested pattern can bind the whole component (Point start) or destructure it further. You choose per component, at each level.

Written without patterns, that first case is if (value instanceof Line l && l.start().x() == l.end().x()) plus four accessor calls in the body. The pattern says the same thing in the shape of the data.

Type matching in nested patterns

record Box(Object contents) { }

class Demo {
    String unwrap(Object value) {
        return switch (value) {
            case Box(String s) -> "boxed text: " + s;        // matches only if contents is a String
            case Box(Integer i) -> "boxed int: " + i;
            case Box(Object o) -> "boxed something: " + o;
            default -> "not a box";
        };
    }
}

A nested pattern is a type test as well as a binding. Box(String s) fails to match a Box holding an Integer and falls through to the next case — so this reads as a decision table over the contents, not just the container.

Two things to know

The component types must match exactly for primitives. Writing Point(long x, long y) against a record declared with int does not compile, even though the widening would be safe. Use var if you would rather not repeat the types.

Record patterns do not match null components. A nested pattern with a type test fails on null, which is usually what you want but is worth knowing:

record Box(Object contents) { }

class Demo {
    void run() {
        Object value = new Box(null);

        if (value instanceof Box(String s)) {
            System.out.println("text: " + s);      // does NOT run
        }
        if (value instanceof Box(var anything)) {
            System.out.println("matched, contents = " + anything);   // this does
        }
    }
}

A var pattern matches anything including null; a typed pattern does not.

When to use them

Record patterns are at their best where data arrives as a shape you did not design — a parsed message, an event, an expression tree — and you are dispatching on both its type and its contents. Combined with sealed interfaces they give you in plain Java what other languages call algebraic data types and exhaustive matching.

They are not a reason to turn well-behaved objects into records so you can destructure them. If behaviour belongs on the type, put it there; the OOP argument does not stop applying because a new syntax exists.

Next

Sequenced Collections is next — a small addition that finally gives every ordered collection a first and last element.