CodeGym /Courses /JAVA 25 SELF /Record Patterns (Java 21+): syntax, examples

Record Patterns (Java 21+): syntax, examples

JAVA 25 SELF
Level 65 , Lesson 3
Available

1. Diving deeper into record classes

Before diving into record patterns, let’s refresh what a record class is.

A record is a compact, immutable class with auto-generated equals, hashCode, toString methods, as well as automatically generated accessors for all components. Record classes arrived in Java 16 and have been a real gift for everyone tired of hand-writing DTOs.

// Classic: Point with two fields
record Point(int x, int y) {}

Creating an object and accessing fields:

Point p = new Point(10, 20);
System.out.println(p.x()); // 10
System.out.println(p.y()); // 20
System.out.println(p);     // Point[x=10, y=20]

Why record patterns

Before Java 21, if you wanted to check that an object was a certain record and get its fields, you had to write a lot of boilerplate: type check via instanceof, cast, and call accessors.

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

If the record is complex or nested, the code quickly turns into a jungle of unpacking. And using such recognition in switch used to be awkward.

Record patterns let you destructure values of record classes directly in pattern matching: both in instanceof and in switch. This makes the code shorter, safer, and easier to read.

2. Record pattern syntax: the simplest example

record Point(int x, int y) {}

Object obj = new Point(10, 20);

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

What’s happening here?

  • obj instanceof Point(int x, int y) — we check the type and immediately destructure the components into variables x and y.
  • The variables x and y are available only inside the if block.

Output:

x=10, y=20

Note: if obj is not a Point or is null, the if block won’t execute and the variables won’t be declared.

3. Record patterns in switch: conciseness and safety

record Point(int x, int y) {}
record Circle(Point center, int radius) {}

Object shape = new Point(3, 4);

switch (shape) {
    case Point(int x, int y) -> System.out.println("Point: x=" + x + ", y=" + y);
    case Circle(Point center, int r) -> System.out.println("Circle with radius " + r);
    default -> System.out.println("Unknown shape!");
}
  • In the case Point(int x, int y) we immediately get access to x and y.
  • In the case Circle(Point center, int r) we have center and r.

Output:

Point: x=3, y=4

With sealed hierarchies, a switch becomes exhaustive: if you forget to handle a variant, the compiler will warn you.

4. Nested record patterns: unpacking inside unpacking

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

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

if (obj instanceof Line(Point(int x1, int y1), Point(int x2, int y2))) {
    System.out.println("Line from (" + x1 + "," + y1 + ") to (" + x2 + "," + y2 + ")");
}

Output:

Line from (1,2) to (3,4)

Working with trees, graphs, complex structures? Nested patterns are your friends.

5. Record patterns with guard expressions (when)

Object obj = new Point(0, 100);

if (obj instanceof Point(int x, int y) && x == 0) {
    System.out.println("Point lies on the Y-axis: y=" + y);
}

In switch this is done using when:

switch (obj) {
    case Point(int x, int y) when x == 0 -> System.out.println("On the Y-axis: y=" + y);
    case Point(int x, int y) when y == 0 -> System.out.println("On the X-axis: x=" + x);
    case Point(int x, int y) -> System.out.println("Regular point");
    default -> System.out.println("Not a point");
}

6. Applying record patterns in a real application

Let’s take a practical example — geometric shapes.

record Point(int x, int y) {}
record Circle(Point center, int radius) {}
record Rectangle(Point topLeft, int width, int height) {}
public static void printShapeInfo(Object shape) {
    switch (shape) {
        case Point(int x, int y) -> System.out.println("Point: (" + x + ", " + y + ")");
        case Circle(Point(int x, int y), int radius) ->
            System.out.println("Circle: center=(" + x + ", " + y + "), radius=" + radius);
        case Rectangle(Point(int x, int y), int width, int height) ->
            System.out.println("Rectangle: topLeft=(" + x + ", " + y + "), size=" + width + "x" + height);
        default -> System.out.println("Unknown shape");
    }
}

Example call:

printShapeInfo(new Rectangle(new Point(5, 10), 20, 30));

Output:

Rectangle: topLeft=(5, 10), size=20x30

7. Limitations and caveats of using record patterns

Record classes only. Record patterns work only with objects that are actually record classes.

class NotARecord {
    int a, b;
    NotARecord(int a, int b) { this.a = a; this.b = b; }
}

// Error! Not a record
// if (obj instanceof NotARecord(int a, int b)) { ... }

Component count and types must match. The pattern must match the components of the record class.

record Pair(int a, String b) {}

// Error: types do not match
// if (obj instanceof Pair(String a, int b)) { ... }

Variables are available only inside the block. Names declared in the pattern are visible in the body of the specific if or case where the match succeeded.

8. Nested patterns: example with an expression tree

sealed interface Expr permits NumberExpr, PlusExpr, MinusExpr {}

record NumberExpr(int value) implements Expr {}
record PlusExpr(Expr left, Expr right) implements Expr {}
record MinusExpr(Expr left, Expr right) implements Expr {}
int eval(Expr expr) {
    return switch (expr) {
        case NumberExpr(int value) -> value;
        case PlusExpr(Expr left, Expr right) -> eval(left) + eval(right);
        case MinusExpr(Expr left, Expr right) -> eval(left) - eval(right);
    };
}

Usage example:

Expr e = new PlusExpr(new NumberExpr(7), new MinusExpr(new NumberExpr(10), new NumberExpr(3)));
System.out.println(eval(e)); // 7 + (10 - 3) = 14

Here we immediately destructure NumberExpr(int value), PlusExpr(Expr left, Expr right), and MinusExpr(Expr left, Expr right) — the code becomes concise and expressive.

9. Table: comparing pattern matching with and without record patterns

Approach Manual type casting Pattern matching with record patterns
Type check
if (obj instanceof ...)
if (obj instanceof Point(int x, int y))
Type cast
(Point) obj
Not needed
Field access
p.x(), p.y()
x, y
Nested unpacking Lots of manual code Nested patterns
Use in switch Inconvenient/impossible Easy, concise

10. Common mistakes when using record patterns

Error #1: trying to apply a record pattern to a non-record class. If you wrote a regular class and then try to “destructure” it as a record, the compiler won’t allow it. Record patterns work only with real record classes.

Error #2: mismatched number or types of components. The pattern must fully match the component signature of the record. For example, for record Pair(int a, String b) you cannot write Pair(int x, int y).

Error #3: trying to use pattern variables outside the block. Names declared in the pattern are available only inside the corresponding if/case. Outside the block such variables do not exist.

Error #4: using record patterns on old JDK versions. Support arrived in Java 21. On Java 17 and earlier you’ll get a syntax error. Check your JDK version and IDE support.

Error #5: overly deep nested patterns. Nesting is powerful, but don’t overcomplicate unless necessary: too deep a structure hurts readability and raises the risk of mistakes.

1
Task
JAVA 25 SELF, level 65, lesson 3
Locked
Geometric editor: description of point positions 📍
Geometric editor: description of point positions 📍
1
Task
JAVA 25 SELF, level 65, lesson 3
Locked
Calculator Brain: Evaluating Complex Expressions 🧠➕➖
Calculator Brain: Evaluating Complex Expressions 🧠➕➖
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION