Topic 11.3
Record Patterns
In one line
A record pattern like Point(int x, int y) checks that a value is a Point and pulls out its components into variables in one step. Record patterns nest, so Line(Point(var x1, var y1), Point p2) matches and unpacks a whole tree of records at once, in instanceof and in switch.
Think of it like this
A tiffin box with labelled compartments. When you open it you don't just say "this is a tiffin"; in one look you see "rice here, dal there, roti in the third". If one compartment holds another small box, you can look inside that too. A record pattern opens a record and names what is in each compartment, as deep as you want to look.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Record pattern
- A pattern written like a record's header, such as
Point(int x, int y). It checks the type and pulls out each component into a variable. - Deconstruction
- Taking an object apart into its components: the opposite of constructing it from them.
- Nested pattern
- A pattern inside a component slot of a record pattern, such as
Point(var x, var y)insideLine(Point(var x, var y), Point p). - var pattern
var xin a component slot. It matches anything, including null, and givesxthe component's declared type.- Algebraic data type
- A type defined as a fixed set of shapes, each with its own data, like "an expression is a number, a sum, or a product". In Java: a sealed interface whose subtypes are records.
- Accessor
- The method a record generates for each component, named after it, like
x(). Record patterns call these to read components. - Type inference
- The compiler working out a type you didn't write, such as the
<String, Integer>inPair(var a, var b).
Step by step
01From type pattern to record pattern
With a type pattern you match and then ask for each component. With a record pattern you match and receive the components directly. The two forms below do the same thing; the second is shorter and cannot forget a component.
The names in the pattern are your choice: Point(int a, int b) binds a and b. They don't have to match the component names, though matching names usually reads best.
record Point(int x, int y) { }
Object o = new Point(3, 4);
if (o instanceof Point p) { // type pattern (Java 16)
System.out.println(p.x() + p.y()); // 7
}
if (o instanceof Point(int x, int y)) { // record pattern (Java 21)
System.out.println(x + y); // 7
}02Nesting: look several levels deep
A component slot can hold another record pattern. To check "a line whose start point is on the x axis" you write the shape you expect, and the compiler generates the nested type tests and accessor calls.
var is handy in nested slots: it takes the component's declared type, so Point(var x, var y) gives two ints.
record Line(Point from, Point to) { }
static String start(Object o) {
if (o instanceof Line(Point(var x, var y), Point end) && y == 0) {
return "starts on the x axis at " + x + ", ends at " + end;
}
return "something else";
}03An expression tree, the classic example
Model arithmetic as a sealed interface Expr with records Num, Add, Mul, Neg. Evaluating is a switch with one case per record, each case unpacking its children and recursing. No default is needed: the compiler knows the four shapes.
Nested patterns make rewrite rules read like algebra: case Mul(Num(int v), Expr x) when v == 1 -> x says "1 times anything is that thing". case Neg(Neg(Expr x)) -> x says "minus minus x is x". Compilers, query planners and rules engines are full of rules like these.
sealed interface Expr permits Num, Add, Mul, Neg { }
record Num(int value) implements Expr { }
record Add(Expr left, Expr right) implements Expr { }
record Mul(Expr left, Expr right) implements Expr { }
record Neg(Expr inner) 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);
case Neg(Expr inner) -> -eval(inner);
};
}04null inside a record
A record pattern needs a real object, so null instanceof Box(var v) is false. Components are different: a record can hold a null component.
A nested pattern that covers the component's whole declared type (var v, or Object x for an Object component) is unconditional and matches the null. A narrower type pattern (String s for an Object component) and any nested record pattern don't. So case Box(String s) skips new Box(null), and a later case Box(var v) catches it with v == null.
record Box(Object content) { }
Object o = new Box(null);
o instanceof Box(String s) // false: String s needs a non-null String
o instanceof Box(Object x) // true: covers every Object, x == null
o instanceof Box(var v) // true: same, v is an Object, v == null05Generic records and inference
For record Pair<A, B>(A first, B second), you can write Pair<String, Integer>(var a, var b), but you rarely need to: in o instanceof Pair(var a, var b) the compiler infers the type arguments from what o can be. Over a Pair<Opt<String>, Opt<String>>, a nested Some(var a) gets a typed as String.
Inference applies to record patterns, not to plain type patterns: a nested None n is a raw type and draws a rawtypes lint warning, while None() (a record pattern with zero components) is inferred.
06Exhaustiveness through the nesting
When components are sealed types, the compiler checks combinations. For a Pair of two Opts (each Some or None), the four cases (Some, Some), (Some, None), (None, Some), (None, None) are exhaustive. Drop one and javac says the switch expression does not cover all possible input values.
This is the compiler doing the case analysis you'd otherwise do with nested ifs, and checking it.
07Where record patterns can and can't go
Record patterns work in instanceof and in case labels, including with guards. They don't work in an enhanced for header: Java 20's preview allowed for (Point(var x, var y) : points), but that was dropped before Java 21, and javac now reports a syntax error. Inside the loop body, use if (p instanceof Point(var x, var y)) or a switch.
Only records can be deconstructed. Ordinary classes have no record pattern, because the compiler doesn't know which fields make up their state. Java 22 adds the unnamed pattern _ for components you don't need (Topic 11.4).
Try it yourself
- 1
Add a rule
In the expression example, add a simplification
case Add(Expr x, Num(int v)) when v == 0 -> simplify(x);and buildnew Add(new Num(5), new Num(0)). Predict where to put the new case so it isn't dominated, then run. - 2
Drop a combination
In the generic example, delete the
case Pair(None(), None())line. Predict the compiler error and its line number, then compile. - 3
Swap var for a type
In the null example, change
case Box(var other)tocase Box(Double d). Predict which inputs now fall through todefault, then check.
Code & diagrams
Expected output
expr: (1 * 7 + -(-(3)))
value: 10
simplified: (7 + 3)
value: 10
Mul[left=Num[value=0], right=Num[value=99]] -> Num[value=0]Expected output
box of text: tea
box of number: 5
box of something else: 2.5
box of something else: null
no box
not a box
Box(String s) matches Box(null)? false
Box(Object x) matches Box(null)? true
Box(var v) matches Box(null)? true
Box(var v) matches null? falseExpected output
Asha & Ravi
Asha alone
Ravi alone
nobody
Pair[first=2, second=left]List<Point> points = List.of(new Point(1, 2), new Point(3, 4));
// for (Point(var x, var y) : points) { } // Java 20 preview only; a syntax error in Java 21+
for (Point p : points) { // the Java 21 way
if (p instanceof Point(var x, var y)) {
System.out.println(x + y);
}
}Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Wrong number of components
With record Point(int x, int y), write if (o instanceof Point(int x)).
Break #2
A component type that doesn't fit
Write o instanceof Point(long x, int y) for record Point(int x, int y).
Break #3
Miss a nested combination
Over record Pair(Opt a, Opt b) with sealed Opt = Some | None, write cases for (Some, Some), (Some, None) and (None, None) only.
Myth vs fact
Myth
Record patterns read the private fields directly.
Fact
They call the accessor methods. If you override an accessor, your method runs during matching.
Myth
Box(var v) and Box(String s) behave the same when the content is a String.
Fact
For a String they do, but var v also matches a null component (and any other type), while String s doesn't.
Myth
You can deconstruct any class with a pattern.
Fact
Only records. A normal class has no declared component list, so there's nothing for a pattern to mirror. Deconstruction for classes has been discussed by the Java team but isn't part of Java 25.
Myth
Record patterns work everywhere a variable is declared.
Fact
Only in instanceof and case labels. The enhanced-for form was a Java 20 preview and was removed before Java 21.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
JLS 14.30.2 defines record pattern matching in terms of the accessor methods, and leaves unspecified whether and how often an accessor is invoked for the same record during one
switch. Accessors that log, count or lazily compute are therefore unsafe to rely on during matching. Keep accessors pure. - ▸
If an accessor throws during matching, the exception is wrapped in
MatchException(Java 21) with the original as its cause. That's another reason to validate in the compact constructor rather than in accessors. - ▸
Exhaustiveness for nested patterns is checked by a JLS algorithm that groups patterns by record type and checks each component position, recursing through sealed hierarchies. It handles combinations, but it does not reason about guards: a case with
whennever counts toward exhaustiveness. - ▸
This is data-oriented programming: plain transparent data (records), closed hierarchies (sealed), and operations as exhaustive pattern-matching functions. It complements, not replaces, object-oriented design; Brian Goetz's "Data-Oriented Programming in Java" article (2022) is the canonical overview.
Remember this
- 1
Topic 11.1's type pattern
o instanceof Point ptells you the type, but you still callp.x()andp.y(). A record patterno instanceof Point(int x, int y)matches whenois a non-nullPointand binds each component:xgetsp.x(),ygetsp.y(). This is called deconstruction: the reverse of the constructor. Previewed in Java 19 and 20, record patterns became standard in Java 21 (JEP 440). - 2
Each component slot holds a nested pattern: a type pattern (
int x,String s),var(infers the component type), or another record pattern. SoLine(Point(var x1, var y1), Point(var x2, var y2))matches aLinewhose two components arePoints and binds four numbers in one go. The number of nested patterns must equal the number of components, and their types must fit the component types. - 3
null rules. A record pattern never matches
nullitself. Inside it, a nested type pattern that covers the whole component type (likeObject xfor anObjectcomponent, orvar v) does match anullcomponent, while a narrower one (likeString sfor anObjectcomponent) doesn't, and neither does a nested record pattern. SoBox(var v)matchesnew Box(null)withv == null, butBox(String s)doesn't. - 4
Record patterns work with generic records, and the compiler infers type arguments:
if (o instanceof Pair(var a, var b))works without writingPair<String, Integer>. Combined with sealed interfaces (Topic 5.11) in aswitch(Topic 11.2), the compiler checks exhaustiveness through the nesting:Pair(Some(var a), Some(var b)),Pair(Some(var a), None()),Pair(None(), Some(var b))andPair(None(), None())together cover every pair with nodefault. - 5
Matching calls the record's accessor methods to get component values. If you overrode an accessor, your code runs, and the specification doesn't promise how many times it is called, so accessors with side effects are a bad idea. Matching is otherwise plain bytecode:
instanceof,checkcast, accessor calls and nested tests. - 6
Records plus sealed interfaces plus record patterns are Java's algebraic data types: you describe a closed set of data shapes, and functions over them are exhaustive switches that unpack each shape. Expression trees, JSON values, parse results, commands and events all model naturally this way. Trees in particular map straight onto the DSA course's recursion (
/dsa).
Explain it without notes
What does o instanceof Point(int x, int y) do, step by step?
How do record patterns behave with null, both for the record and for its components?
How does the compiler check exhaustiveness for nested record patterns over sealed types?
Why can't you write a record pattern for an ordinary class?
What is the relationship between records, sealed interfaces and record patterns?
Practice
With record Point(int x, int y) and record Line(Point from, Point to), write kind(Line) returning "a single point", "horizontal", "vertical", "diagonal" (45 degrees) or "sloped", using nested record patterns and guards.
Model a binary tree as sealed interface Tree permits Leaf, Node with record Leaf() and record Node(Tree left, int value, Tree right). Write sum, height and isLeafNode (a node whose children are both leaves) with record patterns.
Model a café bill: sealed interface Item permits Food, Drink, Food(String name, int pricePaise), Drink(String name, int pricePaise, boolean large), and Line(Item item, int qty). Large drinks cost 1000 paise more; 3 or more of the same food get 10% off. Write total(Line) with nested patterns and print a bill.
Trade-offs
- ↔
Deep nested patterns are precise but tie the code to the exact structure of your records. Renaming or reordering a component breaks every pattern that mentions it, so keep nesting shallow in widely shared code and prefer helper methods for deep checks.
- ↔
Algebraic data types make adding an operation easy (one new switch) and adding a shape costly (every switch changes). Classic polymorphism is the opposite. Choose based on which axis changes more often in your domain.
- ↔
Record patterns call accessors and create no new objects, so they are cheap, but a long chain of guarded cases re-runs tests case by case. For hot paths with many cases, measure before assuming a switch is faster than a map lookup or a virtual call.
Done when you can
Done when you can deconstruct a record in
instanceofand in aswitchcase.Done when you can write nested record patterns with
var.Done when you can predict which patterns match a
nullcomponent.Done when you can model a closed domain as sealed interface plus records and write exhaustive functions over it.
Done when you know where record patterns aren't allowed (for headers, ordinary classes).