Java Record Patterns (Java 21+)

Record patterns, standardized in Java 21 (JEP 440), let you deconstruct record components directly inside instanceof tests and switch case labels, without calling accessor methods manually. Combined with sealed classes and pattern matching for switch, they enable remarkably concise and safe data processing code.

Contents

Basic Record Patterns

A record pattern in instanceof simultaneously tests the type and binds the record components to variables:

Java
record Point(int x, int y) {}

Object obj = new Point(3, 7);

// Old way — test + cast + accessor calls
if (obj instanceof Point) {
    Point p = (Point) obj;
    System.out.println(p.x() + ", " + p.y());
}

// With record pattern — test + destructure in one step
if (obj instanceof Point(int x, int y)) {
    System.out.println(x + ", " + y); // x and y bound directly
}

// With guard (when)
if (obj instanceof Point(int x, int y) && x > 0 && y > 0) {
    System.out.println("Point in first quadrant: " + x + ", " + y);
}

// Works with generics
record Box<T>(T value) {}
Object box = new Box<>("hello");
if (box instanceof Box<String>(String s)) {
    System.out.println("Box contains: " + s.toUpperCase());
}
// Use Box<?> for unchecked generic match:
if (box instanceof Box<?>(var v)) {
    System.out.println("Box contains: " + v);
}

Nested Record Patterns

Record patterns can be nested: components that are themselves records can be deconstructed in place, without intermediate variable bindings:

Java
record Point(int x, int y) {}
record Line(Point start, Point end) {}
record Triangle(Point a, Point b, Point c) {}

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

// Nested pattern — deconstruct Line AND its two Points
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.printf("Line from (%d,%d) to (%d,%d) — length=%.2f%n",
        x1, y1, x2, y2, length);
}

// Triple nesting — Triangle containing three Points
static double perimeter(Object shape) {
    if (shape instanceof Triangle(
            Point(int ax, int ay),
            Point(int bx, int by),
            Point(int cx, int cy))) {
        double ab = Math.hypot(bx - ax, by - ay);
        double bc = Math.hypot(cx - bx, cy - by);
        double ca = Math.hypot(ax - cx, ay - cy);
        return ab + bc + ca;
    }
    return 0;
}

Record Patterns in switch

Record patterns are most powerful in switch expressions, where they combine with type dispatch and guarded patterns:

Java
sealed interface Shape permits Circle, Rectangle, Triangle {}
record Circle(double radius)               implements Shape {}
record Rectangle(double width, double height) implements Shape {}
record Triangle(double a, double b, double c)  implements Shape {}

static String describe(Shape shape) {
    return switch (shape) {
        case Circle(double r) when r == 0        -> "Degenerate circle (point)";
        case Circle(double r)                    -> "Circle r=%.2f area=%.2f".formatted(r, Math.PI*r*r);
        case Rectangle(double w, double h) when w == h -> "Square side=" + w;
        case Rectangle(double w, double h)       -> "Rectangle %sx%s".formatted(w, h);
        case Triangle(double a, double b, double c)    -> "Triangle sides %.1f,%.1f,%.1f".formatted(a,b,c);
    };
}

// Functional-style expression evaluator
sealed interface Expr permits Num, Add, Mul, Neg {}
record Num(int v)             implements Expr {}
record Add(Expr l, Expr r)    implements Expr {}
record Mul(Expr l, Expr r)    implements Expr {}
record Neg(Expr e)            implements Expr {}

static int eval(Expr e) {
    return switch (e) {
        case Num(int v)       -> v;
        case Add(var l, var r)-> eval(l) + eval(r);
        case Mul(var l, var r)-> eval(l) * eval(r);
        case Neg(var x)       -> -eval(x);
    };
}
// eval(new Add(new Mul(new Num(2), new Num(3)), new Num(4))) == 10

With Sealed Classes

Combining sealed class hierarchies with record patterns enables exhaustive deconstruction in a single switch expression. Because the compiler knows every permitted subtype of a sealed interface, a switch over that type can be checked for completeness: no default branch is needed, and adding a new subtype later causes a compile error at every switch that covers the hierarchy. Each case can simultaneously match the subtype and destructure the record's components, producing code that closely resembles pattern matching in functional languages and handles algebraic data types cleanly.

Java
sealed interface JsonValue
    permits JsonNull, JsonBool, JsonNumber, JsonString, JsonArray, JsonObject {}

record JsonNull()                                implements JsonValue {}
record JsonBool(boolean value)                   implements JsonValue {}
record JsonNumber(double value)                  implements JsonValue {}
record JsonString(String value)                  implements JsonValue {}
record JsonArray(List<JsonValue> elements)        implements JsonValue {}
record JsonObject(Map<String, JsonValue> fields)  implements JsonValue {}

static String toJavaString(JsonValue json) {
    return switch (json) {
        case JsonNull()           -> "null";
        case JsonBool(var b)      -> Boolean.toString(b);
        case JsonNumber(var n)    -> n % 1 == 0 ? Long.toString((long)n) : Double.toString(n);
        case JsonString(var s)    -> "\"" + s + "\"";
        case JsonArray(var elems) -> elems.stream()
            .map(e -> toJavaString(e))
            .collect(Collectors.joining(", ", "[", "]"));
        case JsonObject(var fields) -> fields.entrySet().stream()
            .map(e -> "\"" + e.getKey() + "\": " + toJavaString(e.getValue()))
            .collect(Collectors.joining(", ", "{", "}"));
    };
}

Using var in Patterns

Use var for component types when the exact type can be inferred by the compiler, reducing verbosity for complex generic or long type names:

Java
record Pair<A, B>(A first, B second) {}
record Triple<A, B, C>(A first, B second, C third) {}

Object obj = new Pair<>(List.of(1, 2, 3), Map.of("key", "value"));

// var infers the types from the record definition
if (obj instanceof Pair<?,?>(var first, var second)) {
    System.out.println("First type:  " + first.getClass().getSimpleName());
    System.out.println("Second type: " + second.getClass().getSimpleName());
}

// Can mix explicit types and var
if (obj instanceof Pair<List<Integer>,?>(List<Integer> nums, var other)) {
    System.out.println("Sum: " + nums.stream().mapToInt(Integer::intValue).sum());
}
Record patterns are purely structural: they do not call equals() or any other method on the components. They only bind the values returned by the accessor methods.