skip to content
everything.modern.recordPatterns

record patterns

destructure records, nested, right in the pattern. plus _.

chapter
chapter 08.06
reading time
7 min read
java 21+

a record pattern takes a record apart right in the pattern: o instanceof Point(int x, int y) checks that o is a Point and pulls out its components. patterns nest, so one pattern can pull all four numbers out of a Line made of two Points. final in java 21.

the unnamed pattern _ java 22+ says "something goes here, i don't care what". it also works for variables you have to declare but never use.

the picture

you know how, after if (o instanceof Line l), the very next lines are always l.start().x(), l.start().y(), l.end().x()... digging back into the thing you just tested? a record pattern does the digging in the test itself.

think of a set of nesting dolls with labels painted inside each lid. a type pattern opens the outer doll and hands you the doll. a record pattern opens it, opens the dolls inside, and hands you the labels, already read. and it only does that if every doll is the shape you said it would be.

pattern
o =
if (o instanceof Line(Point(var x1, var y1), Point(var x2, var y2))) { ... }
the value, on the heap
o: Line
start: Point
x1
y2
end: Point
x4
y6
variables
none yet
what runs
o instanceof Line ✓

first, the outer type test, exactly like instanceof: is o a Line (and not null)? it is.

step 1/8
pick a pattern and a value, then step through. each step is one accessor call or test, in the order javac 25 really makes them (checked by logging from every accessor). amber is what's being looked at, green passed, red failed. variables in blue are references, plain values are green. try Line(Point(1, 2), null) with the deep pattern and then the shallow one.

three things the stepper shows that aren't obvious from the syntax:

  1. it's just accessor calls. a record pattern calls start(), end(), x(), y() for you. records have accessors for every component, which is exactly why patterns work on records (and only on records, for now).
  2. a record pattern never matches null, at any depth. but a plain type pattern for the component's own type (Point s on a Point component) is unconditional, so it does match a null component.
  3. variables are bound all at once, only after everything matched. a failed match leaves nothing behind.

using them

the same work, with and without:

Main.java
record Point(int x, int y) {}
record Line(Point start, Point end) {}
 
static double length(Object o) {
    if (o instanceof Line(Point(var x1, var y1), Point(var x2, var y2))) {
        return Math.hypot(x2 - x1, y2 - y1);
    }
    return -1;
}
 
void main() {
    IO.println(length(new Line(new Point(1, 2), new Point(4, 6))));
    IO.println(length(new Line(new Point(1, 2), null)));
    IO.println(length("not a line"));
}
output
5.0
-1.0
-1.0

the second call returns -1.0, not a NullPointerException: end is null, the nested Point(...) pattern doesn't match it, so the if is simply false. the null check came free with the pattern.

var inside a record pattern means "whatever the component's type is" (here int). you can also write a type, Point(int x, int y), which reads well when it matters. for a reference component it can be narrower than the declared type (Wrap(String s) on an Object component only matches strings). for a primitive component it has to be the exact type in java 25: Point(double x, double y) on int components is a different feature, primitive patterns, still in preview.

in a switch, record patterns combine with sealed types into the shape java code increasingly takes:

Main.java
record Point(int x, int y) {}
sealed interface Shape permits Circle, Rect {}
record Circle(Point center, int r) implements Shape {}
record Rect(Point topLeft, Point bottomRight) implements Shape {}
 
static String describe(Shape s) {
    return switch (s) {
        case Circle(Point(var x, var y), var r) -> "circle r=" + r + " at " + x + "," + y;
        case Rect(Point(var x1, var y1), Point(var x2, var y2)) -> "rect " + (x2 - x1) + "x" + (y2 - y1);
    };
}
 
void main() {
    IO.println(describe(new Circle(new Point(0, 0), 2)));
    IO.println(describe(new Rect(new Point(1, 1), new Point(4, 5))));
}
output
circle r=2 at 0,0
rect 3x4

no default: Shape is sealed, and every subtype has a case. that's the whole pattern for modelling data in java now, with a chapter of its own: data-oriented programming.

record patterns infer type arguments, too. on a Box<String>, case Box(var s) makes s a String, no cast, no <String> in the pattern.

exhaustiveness goes all the way down

when the components of a record are themselves sealed, javac checks every combination. a Pair(Shape a, Shape b) has four (circle-circle, circle-square, square-circle, square-square), and a switch over Pair has to cover all of them:

Main.java
  1. 1String both(Pair p) {
  2. 2 return switch (p) {
  3. 3 case Pair(Circle _, Circle _) -> "...";
  4. 4 case Pair(Circle _, Square _) -> "...";
  5. 5 case Pair(Square _, Circle _) -> "...";
  6. 6 };
  7. 7}
a ↓ b →CircleSquareCircle
case 1
case 2
Square
case 3
nothing
compile error
Main.java:2: error: the switch expression does not cover all possible input values

no case handles Pair(Square, Square), and javac knows, because both components are sealed.

turn case labels on and off. the grid shows which case catches each combination of components, and any combination nothing catches turns red. i compiled all 63 combinations of these six labels with javac 25: it accepts a switch exactly when the grid is all green.

a nested _ or var covers every possibility for its component, which is why Pair(Square _, Shape _) fills a whole row. and like any pattern switch, an exhaustive one has no default, so adding a third Shape tomorrow breaks every switch that doesn't handle it, at compile time.

_: the unnamed pattern and variable

since java 22 (JEP 456), _ means "i have to put something here, and i'm not going to use it". in a pattern, it matches anything (null included) and binds nothing. outside patterns, it's a variable you can declare and never read:

Main.java
record Point(int x, int y) {}
sealed interface Shape permits Circle, Rect {}
record Circle(Point center, int r) implements Shape {}
record Rect(Point topLeft, Point bottomRight) implements Shape {}
 
void main() {
    List<Shape> shapes = List.of(new Circle(new Point(0, 0), 2), new Rect(new Point(0, 0), new Point(3, 4)), new Circle(new Point(5, 5), 1));
 
    int circles = 0;
    for (var s : shapes) if (s instanceof Circle _) circles++;          // a pattern you don't name
 
    int xs = 0;
    for (var s : shapes) if (s instanceof Circle(Point(var x, _), _)) xs += x;   // only x matters
 
    Map<String, Integer> ages = Map.of("ada", 36);
    ages.forEach((_, age) -> IO.println("age " + age));             // an unused lambda parameter
 
    int parsed;
    try { parsed = Integer.parseInt("x"); } catch (NumberFormatException _) { parsed = -1; }   // an unused exception
 
    int count = 0;
    for (var _ : shapes) count++;                                     // an unused loop variable
 
    IO.println(circles + " " + xs + " " + parsed + " " + count);
}
output
age 36
2 5 -1 3

it also unlocks one switch trick: a case can list several patterns, as long as none of them binds a variable. case Circle _, Square _ -> is legal; case String s, Integer i isn't, because which variable would exist on the right of the arrow? and _ is only ever a declaration. reading it (IO.println(_)) is error: underscore not allowed here.

predict the output
java
record Wrap(Object value) {}
 
Object w = new Wrap(null);
IO.println(w instanceof Wrap(Object v));
keylines
  • Point(int x, int y): test the type and pull out the components in one pattern.
  • patterns nest. var takes the component's type, _ ignores it.
  • it's accessor calls underneath. an accessor that throws becomes a MatchException.
  • a record pattern never matches null. a type pattern of the component's own type does.
  • over sealed components, a switch must cover every combination, no default needed.
  • _ java 22+ for unused patterns, lambda parameters, catch variables and loop variables.