Contents
- Basic Record Patterns
- Nested Record Patterns
- Record Patterns in switch
- With Sealed Classes
- Using var in Patterns
A record pattern in instanceof simultaneously tests the type and binds the record components to variables:
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);
}
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 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
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 branch is needed, and adding a new subtype later causes a compile error at every switch that covers the hierarchy. Each case can simultaneously match the subtype and destructure the record's components, producing code that closely resembles pattern matching in functional languages and handles algebraic data types cleanly.
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(", ", "{", "}"));
};
}
Use var for component types when the exact type can be inferred by the compiler, reducing verbosity for complex generic or long type names:
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());
}
Record patterns are purely structural — they do not call equals() or any other method on the components. They only bind the values returned by the accessor methods.