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
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:
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:
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
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
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());
}
equals()