Java 21 Record Patterns with Examples

Introduction RecordsClick to read: Records make it easy to build an object from its components. Record patterns, previewed in Java 19 and 20 and final in Java 21Click to read tutorial: Overview of New Features in Java 21 (JEP 440), make it just as easy to take one apart: a single pattern tests whether an object is a given record and extracts its components into variables - and patterns can be nested to reach into records inside records. This tutorial explains record patterns with examples - deconstructing records in instanceof, nested patterns, using var, combining them with sealed interfaces and switch, and generic records. Deconstructing a record With pattern matching for instanceofClick to read: pattern matching for instanceof, you could test for a record type and then call its accessors. A record pattern lists the record's components in parentheses, and binds each to a variable directly -
Type pattern vs record pattern
//RecordPatternBasics.java
public class RecordPatternBasics {
  record Point(int x, int y) { }

  public static void main(String[] args) {
    Object obj = new Point(3, 4);

    if (obj instanceof Point p) {                       // Java 16: type pattern, then accessors
      System.out.println("x + y = " + (p.x() + p.y()));
    }
    if (obj instanceof Point(int x, int y)) {           // Java 21: record pattern
      System.out.println("x + y = " + (x + y));
    }
    if (obj instanceof Point(var x, var y) && x == y) { // var infers the component types
      System.out.println("on the diagonal");
    } else {
      System.out.println("not on the diagonal");
    }
  }
}
 OUTPUT of the above code
x + y = 7
x + y = 7
not on the diagonal
Point(int x, int y) matches if obj is a Point, and then calls the record's accessors to bind x and y. The components are listed in the record's declaration order, but the variable names are your own. A record pattern never matches null.
Nested patterns The components of a record pattern are patterns themselves, so they can be record patterns too. This lets one pattern describe the shape of a whole data structure -
Reaching into records inside records
//NestedPatterns.java
public class NestedPatterns {
  record Point(int x, int y) { }
  enum Color { RED, GREEN }
  record ColoredPoint(Point point, Color color) { }
  record Line(ColoredPoint start, ColoredPoint end) { }

  public static void main(String[] args) {
    Object obj = new Line(new ColoredPoint(new Point(0, 0), Color.RED),
                          new ColoredPoint(new Point(3, 4), Color.GREEN));

    if (obj instanceof Line(ColoredPoint(Point(var x1, var y1), Color c1),
                            ColoredPoint(Point(var x2, var y2), var c2))) {
      double length = Math.hypot(x2 - x1, y2 - y1);
      System.out.println("Line from " + c1 + " to " + c2 + ", length " + length);
    }
  }
}
 OUTPUT of the above code
Line from RED to GREEN, length 5.0
Without record patterns, this needed a chain like line.end().point().x() for every coordinate, plus null checks at each level. The nested pattern matches only if every level matches, so a null anywhere simply makes it fail. Record patterns in switch Record patterns are most powerful in switchClick to read tutorial: Java 21 Pattern Matching for switch with Examples, especially with a sealed interfaceClick to read: sealed interface: each case deconstructs one kind of record, and the compiler checks that every kind is handled -
Evaluating an expression tree
//ExpressionEvaluator.java
public class ExpressionEvaluator {
  sealed interface Expr permits Num, Add, Mul, Neg { }
  record Num(int value) implements Expr { }
  record Add(Expr left, Expr right) implements Expr { }
  record Mul(Expr left, Expr right) implements Expr { }
  record Neg(Expr operand) 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(Num(int v), Expr r) when v == 0 -> 0;      // nested pattern with a guard: skip work
      case Mul(Expr l, Expr r) -> eval(l) * eval(r);
      case Neg(Expr inner) -> -eval(inner);
    };                                                     // exhaustive: no default needed
  }

  static String show(Expr e) {
    return switch (e) {
      case Num(var v) -> String.valueOf(v);
      case Add(var l, var r) -> "(" + show(l) + " + " + show(r) + ")";
      case Mul(var l, var r) -> show(l) + " * " + show(r);
      case Neg(var inner) -> "-" + show(inner);
    };
  }

  public static void main(String[] args) {
    Expr expr = new Add(new Mul(new Num(2), new Num(5)), new Neg(new Num(3)));
    System.out.println(show(expr) + " = " + eval(expr));
    Expr zero = new Mul(new Num(0), expr);
    System.out.println(show(zero) + " = " + eval(zero));
  }
}
 OUTPUT of the above code
(2 * 5 + -3) = 7
0 * (2 * 5 + -3) = 0
This is the classic way to process tree-shaped data in functional languages, and records, sealed interfaces and patterns now make it natural in Java. Adding a new kind of expression, say Div, makes both switches fail to compile until they handle it.
Generic records For a generic record, the type arguments of a record pattern can be inferred -
Record patterns with generic records
//GenericRecordPatterns.java
public class GenericRecordPatterns {
  record Pair<A, B>(A first, B second) { }

  static String describe(Pair<String, Integer> pair) {
    if (pair instanceof Pair(var name, var age)) {        // Pair<String, Integer> is inferred
      return name + " is " + age;
    }
    return "?";
  }

  public static void main(String[] args) {
    System.out.println(describe(new Pair<>("Tom", 30)));
    Object o = new Pair<>("key", 42);
    if (o instanceof Pair<?, ?>(String k, Integer v)) {   // test the component types too
      System.out.println(k + " -> " + (v + 1));
    }
  }
}
 OUTPUT of the above code
Tom is 30
key -> 43
Note - Java 20's preview also allowed record patterns in enhanced for loops; that part was dropped before the feature became final in Java 21. Java 22 added the unnamed pattern _Click to read: unnamed pattern _ for components you do not need, as in Point(var x, _). Summary In this tutorial we saw how record patterns, final in Java 21, test for a record type and extract its components in one step. We deconstructed a record in instanceof, used var for the component types, nested patterns to reach into records inside records, and combined record patterns with a sealed interface and switch to evaluate an expression tree exhaustively. Lastly, we used record patterns with generic records. Next, read about pattern matching for switchClick to read tutorial: Java 21 Pattern Matching for switch with Examples.