Record Patterns in Java
A record pattern tests whether a value is an instance of a record and takes it apart into its components in the same step. Writing o instanceof Point(int x, int y) checks that o is a Point and declares x and y holding its components, with no calls to p.x() and p.y(). Record patterns can be nested, so one pattern can look several levels deep into a data structure. They became standard in Java 21 (JEP 440). In this lesson, you will learn the syntax, nesting, var, generics, how record patterns work in switch, and how they affect exhaustiveness.
Syntax
RecordName(Pattern1, Pattern2, ...)Each inner pattern matches one component, in the order the components are declared. An inner pattern can be:
| Inner pattern | Example | Meaning |
|---|---|---|
| A type pattern | int x, String name | Bind the component to a variable |
var | var x | Bind it, letting the compiler infer the type |
| Another record pattern | Point(int x, int y) | Take the component apart too |
| The unnamed pattern | _ | Ignore the component (see "Unnamed Variables and Patterns") |
Example: Nested Patterns, Var and Generics
Let's see an example that takes apart a Line made of two Points, and then a generic Box:
public class Shapes {
record Point(int x, int y) { }
record Line(Point start, Point end) { }
record Box<T>(T content) { }
public static void main(String[] args) {
Object o = new Line(new Point(0, 0), new Point(3, 4));
if (o instanceof Line(Point(var x1, var y1), Point(int x2, int y2))) {
System.out.println("dx=" + (x2 - x1) + " dy=" + (y2 - y1));
}
Box<String> b = new Box<>("Navika");
if (b instanceof Box(var s)) {
System.out.println(s.toUpperCase());
}
}
}dx=3 dy=4
NAVIKAExplanation:
- Nesting:
Line(Point(...), Point(...))checks thatois aLine, that both components arePoints, and binds all four coordinates in one condition. var:var x1takes the component's declared type,int. You can mixvarand explicit types freely.- Generics:
Box(var s)doesn't repeat<String>. The compiler infersBox<String>from the type ofb, sosis aStringands.toUpperCase()compiles.
How Components Are Read
A record pattern gets each component by calling the record's accessor method. If you override an accessor, the pattern uses your version:
public class Accessor {
record Student(String name, int marks) {
public String name() {
System.out.println(" name() called");
return name;
}
}
public static void main(String[] args) {
Object o = new Student("Advitha", 88);
if (o instanceof Student(String n, int m)) {
System.out.println(n + " " + m);
}
}
} name() called
Advitha 88Explanation: The pattern called name() to fill n. That's why record patterns work only with records: a record's components and accessors are known exactly, while an ordinary class has no such guarantee. Using a record pattern on a normal class is an error: "deconstruction patterns can only be applied to records, P is not a record".
Record Patterns in Switch
Record patterns are most useful in switch. Here is the calculator from "Exhaustive Switch with Sealed Types", rewritten so each case takes its record apart directly, plus a simplify operation that looks inside nested records:
public class Calc2 {
sealed interface Expr permits Num, Add, Mul { }
record Num(int value) implements Expr { }
record Add(Expr left, Expr right) implements Expr { }
record Mul(Expr left, Expr right) 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(Expr l, Expr r) -> eval(l) * eval(r);
};
}
static Expr simplify(Expr e) {
return switch (e) {
case Mul(Num(int v), Expr r) when v == 0 -> new Num(0);
case Mul(Expr l, Num(int v)) when v == 0 -> new Num(0);
case Mul(Num(int v), Expr r) when v == 1 -> simplify(r);
case Add(Num(int v), Expr r) when v == 0 -> simplify(r);
case Add(Expr l, Expr r) -> new Add(simplify(l), simplify(r));
case Mul(Expr l, Expr r) -> new Mul(simplify(l), simplify(r));
case Num n -> n;
};
}
public static void main(String[] args) {
Expr e = new Add(new Num(0), new Mul(new Num(1), new Num(7)));
System.out.println(e);
System.out.println(simplify(e));
System.out.println(eval(e));
}
}Add[left=Num[value=0], right=Mul[left=Num[value=1], right=Num[value=7]]]
Num[value=7]
7Explanation: 0 + (1 * 7) is simplified in two steps. Add(Num(int v), Expr r) when v == 0 matches an addition whose left side is the number 0, and returns the simplified right side. Then Mul(Num(int v), Expr r) when v == 1 matches 1 * 7 and returns 7. Without record patterns, each rule would need several instanceof checks and accessor calls. The last three unguarded cases make the switch exhaustive.
Exhaustiveness with Nested Patterns
The compiler checks exhaustiveness through nested patterns too. With sealed interface Pet permits Cat, Dog and record Pair(Pet first, Pet second), all four combinations must be covered:
public class Pets {
sealed interface Pet permits Cat, Dog { }
record Cat(int age) implements Pet { }
record Dog(int age) implements Pet { }
record Pair(Pet first, Pet second) { }
static String pair(Pair p) {
return switch (p) {
case Pair(Cat a, Cat b) -> "two cats";
case Pair(Dog a, Dog b) -> "two dogs";
case Pair(Cat a, Dog b) -> "cat then dog";
case Pair(Dog a, Cat b) -> "dog then cat";
};
}
public static void main(String[] args) {
System.out.println(pair(new Pair(new Dog(3), new Cat(2))));
System.out.println(pair(new Pair(new Cat(2), new Cat(5))));
}
}dog then cat
two catsLeave out the last case and the compiler notices that Pair(Dog, Cat) isn't handled:
R2.java:7: error: the switch expression does not cover all possible input values
return switch (p) {
^
1 errorThe Rules
| Rule | Error if broken |
|---|---|
| The pattern must list every component, in order | "incorrect number of nested patterns" |
| Only records can be deconstructed | "deconstruction patterns can only be applied to records" |
| Each nested type must be possible for that component | "incompatible types" |
Nested type patterns match null only if they are total | (see "Handling Null in Switch") |
For record Point(int x, int y), writing Point(int x) fails:
R3.java:5: error: incorrect number of nested patterns
if (o instanceof Point(int x)) { }
^
required: int,int
found: int
1 errorCommon Mistakes
Leaving out components. Every component needs a pattern; use _ for the ones you don't need.
Getting the order wrong. Components match by position, not by name; Point(int y, int x) binds y to the x-coordinate.
Expensive or side-effecting accessors. Patterns call accessors, so keep them simple. If one throws, the switch throws MatchException.
Trying to deconstruct a normal class. Only records support record patterns.
Interview Corner
What are record patterns?
Record patterns (Java 21) test whether a value is an instance of a record and extract its components into variables in one step, for example if (o instanceof Point(int x, int y)). They can be nested (Line(Point(var x1, var y1), Point p2)), use var, infer generic type arguments, and appear in switch cases with guards. Components are read through the accessor methods. The compiler checks exhaustiveness through nested patterns, so a switch over records with sealed component types can omit default.
Practice
- Easy: Write
record Rectangle(Point topLeft, Point bottomRight)and compute its area with a single nested record pattern. - Harder: Add a rule to
simplifythat turnsMul(x, Num(1))intosimplify(x). Test it on(3 + 0) * 1.
Key Takeaways
Was this tutorial helpful?