Record Patterns
Record patterns, finalized in Java 21 (JEP 440) alongside pattern matching for switch, extend type patterns specifically for records: rather than only confirming a value is an instance of a record type and binding the whole record to a single variable, a record pattern destructures the record's components directly within the pattern itself, binding each component to its own variable in the same step as the type test — and because a record's component can itself be another record, record patterns nest arbitrarily, letting deeply nested data be fully destructured in a single, visually tree-shaped pattern. This entry covers exactly what separate accessor calls record patterns eliminate, nested destructuring syntax and how it mirrors the data's own shape, using var for component types versus explicit types, and record patterns used directly in instanceof versus within switch's pattern-matching cases.
Motivation — Eliminating Accessor Calls After a Type Match
// ── Before record patterns — type match, then separate accessor calls ──
record Point(int x, int y) {}
void printOld(Object obj) {
if (obj instanceof Point p) { // type-only match — whole record bound to "p"
int x = p.x(); // separate accessor call #1
int y = p.y(); // separate accessor call #2
System.out.println("x=" + x + ", y=" + y);
}
}
// ── Java 21 — record pattern destructures components in the SAME step ──
void printNew(Object obj) {
if (obj instanceof Point(int x, int y)) { // type test AND destructuring, one step
System.out.println("x=" + x + ", y=" + y); // x, y already bound — no accessor calls
}
}Nested Destructuring — Patterns That Mirror the Data's Shape
// ── Nested records — naturally tree-shaped domain data ─────────────────
record Point(int x, int y) {}
record Location(String city, Point coordinates) {}
record Event(String name, Location location) {}
// ── Fully nested record pattern — destructures THROUGH every level ─────
void describe(Object obj) {
if (obj instanceof Event(String name, Location(String city, Point(int x, int y)))) {
// name, city, x, y ALL bound here — three levels of nesting destructured in one pattern,
// mirroring the actual Event -> Location -> Point structure visually
System.out.println(name + " in " + city + " at (" + x + "," + y + ")");
}
}
// Contrast — the pre-record-pattern equivalent, accessor chains reconstructing the same nesting:
void describeOld(Object obj) {
if (obj instanceof Event e) {
String name = e.name();
String city = e.location().city();
int x = e.location().coordinates().x();
int y = e.location().coordinates().y();
System.out.println(name + " in " + city + " at (" + x + "," + y + ")");
}
}
// ── Partial destructuring — mix nested patterns and plain bindings freely
void describePartial(Object obj) {
if (obj instanceof Event(String name, Location loc)) {
// "loc" left as a whole Location here — NOT further destructured at this position,
// while "name" IS pulled out directly — author controls depth per-component
System.out.println(name + " happened in " + loc.city());
}
}var in Record Patterns, and Usage in instanceof vs switch
// ── var within a record pattern — inferred per-component ───────────────
record Point(int x, int y) {}
void printVar(Object obj) {
if (obj instanceof Point(var x, var y)) { // both inferred as int — still fully type-checked
System.out.println(x + y);
}
}
// Mixing var and explicit types within the same pattern is also allowed:
record Measurement(String label, double value) {}
void printMixed(Object obj) {
if (obj instanceof Measurement(String label, var value)) {
System.out.println(label + ": " + value);
}
}
// ── Same record pattern syntax/semantics in instanceof vs switch ───────
sealed interface Shape permits Circle, Rectangle {}
record Circle(Point center, double radius) implements Shape {}
record Rectangle(Point topLeft, Point bottomRight) implements Shape {}
// instanceof — single, one-off conditional test:
void checkOne(Shape shape) {
if (shape instanceof Circle(Point(var cx, var cy), var radius)) {
System.out.println("Circle at (" + cx + "," + cy + "), r=" + radius);
}
}
// switch — dispatching across multiple record shapes, WITH exhaustiveness:
double area(Shape shape) {
return switch (shape) {
case Circle(Point(var cx, var cy), var radius) -> Math.PI * radius * radius;
case Rectangle(Point(var x1, var y1), Point(var x2, var y2)) ->
Math.abs((x2 - x1) * (y2 - y1));
// no default — Circle/Rectangle is Shape's complete permitted set,
// compiler-verified exhaustive, exactly as with non-destructuring sealed switches
};
}