Record Patterns & Deconstruction
Reviewed & published by Brayan K
A record pattern matches a record and pulls its components straight out into named variables in one step — and because patterns nest, it can take an entire data structure apart inside a single instanceof or switch.
Part of the free Java course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
Before You Start
You should know records, pattern matching, and sealed classes. Record patterns are standard from Java 21.
What You'll Learn
- Deconstructing a record in instanceof
- Deconstructing in a switch case
- Nested record patterns for deep structures
- Using var for component bindings
- Combining record patterns with when guards
- Sealed + records + patterns = exhaustive switches
The Big Idea — Unpack the Box at the Door
💡 Analogy: A type pattern hands you a sealed box and says "this is a Point" — you still have to open it (p.x(), p.y()) to use what's inside. A record pattern unpacks the box at the door: Point(int x, int y) sets out the contents — x and y — ready to use. And if a box contains smaller boxes, nested patterns unpack those too, in one motion.
The result reads like the data's own shape, top to bottom, instead of a string of accessor calls.
1️⃣ Deconstructing in instanceof
Write the record's name followed by a pattern for each component: obj instanceof Point(int x, int y). If obj is a Point, you immediately get x and y — no accessor calls, no cast. Each component sub-pattern can be an explicit type or var.
public class Main {
record Point(int x, int y) {}
public static void main(String[] args) {
Object obj = new Point(3, 4);
// Record pattern: match Point AND deconstruct it into x and y.
if (obj instanceof Point(int x, int y)) {
System.out.println("x=" + x + ", y=" + y);
System.out.println("distance^2 from origin = " + (x * x + y * y));
}
// Without record patterns you'd bind the whole Point, then call
// accessors p.x() and p.y() yourself.
}
}2️⃣ Nested Record Patterns
Because a record's components can be records, patterns nest. A Line made of two Points can be torn down to its four numbers in a single pattern. Notice how the pattern's shape mirrors the data's shape exactly.
Line(Point(var x1, var y1), Point(var x2, var y2))
^outer record ^inner record ^inner record
bind x1,y1 bind x2,y2public class Main {
record Point(int x, int y) {}
record Line(Point start, Point end) {}
public static void main(String[] args) {
Object shape = new Line(new Point(0, 0), new Point(3, 4));
// Nested record patterns deconstruct all the way down in one go.
if (shape instanceof Line(Point(var x1, var y1), Point(var x2, var y2))) {
int dx = x2 - x1, dy = y2 - y1;
double len = Math.sqrt(dx * dx + dy * dy);
System.out.println("from (" + x1 + "," + y1 + ") to (" + x2 + "," + y2 + ")");
System.out.println("length = " + len);
}
}
}3️⃣ Record Patterns + Sealed = Exhaustive switch
This is the modern Java sweet spot. A sealed interface of record subtypes, processed by a switch whose arms are record patterns: each arm deconstructs its case, and the compiler proves you've covered them all — so no default.
public class Main {
sealed interface Shape permits Circle, Rect {}
record Circle(double r) implements Shape {}
record Rect(double w, double h) implements Shape {}
static String describe(Shape s) {
// Record patterns inside a switch over a SEALED type:
// deconstruct and stay exhaustive (no default).
return switch (s) {
case Circle(double r) -> "circle r=" + r + " area=" + (Math.PI * r * r);
case Rect(double w, double h) -> "rect " + w + "x" + h + " area=" + (w * h);
};
}
public static void main(String[] args) {
System.out.println(describe(new Circle(2)));
System.out.println(describe(new Rect(3, 4)));
}
}4️⃣ Combining with Guards and Types
Component sub-patterns can be specific types, and the whole pattern can carry a when guard. Order matters: more specific patterns and guarded arms come first. Below, two Strings, two equal ints, two ints, and a mixed pair are each handled distinctly.
public class Main {
record Pair(Object first, Object second) {}
static String classify(Object o) {
// Record patterns combine with guards (when) and type patterns.
return switch (o) {
case Pair(String a, String b) -> "two strings: " + a + ", " + b;
case Pair(Integer a, Integer b) when a.equals(b) -> "equal ints: " + a;
case Pair(Integer a, Integer b) -> "two ints: " + (a + b);
case Pair(var a, var b) -> "mixed: " + a + " & " + b;
default -> "not a pair";
};
}
public static void main(String[] args) {
System.out.println(classify(new Pair("hi", "there")));
System.out.println(classify(new Pair(5, 5)));
System.out.println(classify(new Pair(2, 3)));
System.out.println(classify(new Pair("x", 9)));
System.out.println(classify("not a pair"));
}
}🧠 Quick Recall
Predict the result before opening each answer.
record P(int a, int b) {}
Object o = new P(7, 2);
if (o instanceof P(var a, var b))
System.out.println(a - b);Answer: 5. The pattern matches and binds a=7, b=2, so a - b is 5.
record P(int a, int b) {}
if (o instanceof P(var a)) { }Answer: No. A record pattern must bind every component, so a 2-component record needs two sub-patterns. error: record pattern P does not match. Use P(var a, var b).
case Pair(Integer a, Integer b) when a.equals(b) -> "eq";
case Pair(Integer a, Integer b) -> "ne";Answer: "eq". The guarded arm comes first and 4 equals 4, so it wins before the general two-ints arm is considered.
🎯 Your Turn — Deconstruct a Name
Use a record pattern in instanceof to pull out first and last and build a greeting.
public class Main {
record Name(String first, String last) {}
static String greet(Object o) {
// YOUR TURN: use a record pattern in instanceof to pull out first and
// last, then return "Hello, FIRST LAST!". For anything else, return
// "Hello, stranger!".
//
// Expected:
// Hello, Ada Lovelace!
// Hello, stranger!
return "TODO";
}
public static void main(String[] args) {
System.out.println(greet(new Name("Ada", "Lovelace")));
System.out.println(greet("???"));
}
}🧩 Mini-Challenge — Sum a binary tree
Combine sealed records and nested record patterns to total a tree of integers recursively.
public class Main {
// MINI-CHALLENGE: sum a binary tree of ints using sealed + record patterns.
//
// sealed interface Tree permits Leaf, Node {}
// record Leaf(int value) implements Tree {}
// record Node(Tree left, Tree right) implements Tree {}
//
// Write sum(Tree) with an EXHAUSTIVE switch that deconstructs:
// case Leaf(int v) -> v
// case Node(Tree l, Tree r) -> sum(l) + sum(r)
//
// Expected output:
// sum = 6
public static void main(String[] args) {
// Tree t = new Node(new Node(new Leaf(1), new Leaf(2)), new Leaf(3));
// System.out.println("sum = " + sum(t));
}
}Common Errors
- ❌ Wrong number of component patterns. A record pattern must bind every component. Fix: one sub-pattern per component.
- ❌ Using record patterns on a non-record. Only records can be deconstructed this way. Fix: make the type a record, or use a type pattern.
- ❌ Incompatible component type. Point(String x, ...) when x is an int. Fix: match the declared type or use var.
- ❌ Broad pattern before a guarded one. The guarded arm becomes unreachable. Fix: guarded/specific arms first.
- ❌ Forgetting case null in a pattern switch. A record pattern won't match null. Fix: add case null if needed.
- ❌ Running on Java 20 or earlier. Record patterns are standard only from Java 21. Fix: compile with Java 21+.
Pro Tips
- 💡 Use var in deep nests to keep the pattern readable; reserve explicit types for the components you want to constrain.
- 💡 Let the pattern mirror the data. When the pattern's shape matches the record's shape, the code documents the structure for free.
- 💡 Pair with sealed types for exhaustiveness — the compiler becomes a checklist that catches every unhandled case.
- 💡 Great for parsing and ASTs. Expression trees, JSON nodes, and protocol messages all deconstruct cleanly.
📋 Quick Reference
| Concept | Syntax | Note |
|---|---|---|
| instanceof deconstruct | o instanceof Point(int x, int y) | Binds x, y |
| switch deconstruct | case Point(int x, int y) -> ... | No cast, no accessors |
| var components | Point(var x, var y) | Types inferred |
| Nested pattern | Line(Point(var x, var y), ...) | Deep deconstruction |
| Typed component | Pair(Integer a, Integer b) | Constrains the type |
| Guarded | case P(var a, var b) when ... | Adds a boolean test |
| All components required | one sub-pattern each | Arity must match |
| Over sealed types | arm per record subtype | Exhaustive, no default |
| Null | case null -> ... | Pattern won't match null |
| Since | Java 21 | JEP 440 |
📚 Resources / Keep Learning
- 📘 Oracle: Record Patterns — the official language guide.
- 📗 dev.java: Record Patterns — a guided tutorial.
- 📙 JEP 440: Record Patterns — the finalizing proposal.
🎉 Lesson Complete!
You can now deconstruct records inline with record patterns, nest them to take apart whole structures, use var for components, add when guards, and combine sealed types, records, and patterns into exhaustive switches.
Practice quiz
What does a record pattern like Point(int x, int y) do?
- Creates a new Point
- Calls a constructor
- Matches a Point and binds its components to x and y
- Compares two points
Answer: Matches a Point and binds its components to x and y. A record pattern matches the type and deconstructs it, binding each component to a variable in one step.
In which Java version did record patterns become a standard feature?
- Java 21
- Java 16
- Java 17
- Java 11
Answer: Java 21. Record patterns were finalized in Java 21 (JEP 440).
What is a nested record pattern?
- A record inside a method
- Two records in a switch
- A recursive method
- A pattern that deconstructs a record whose components are themselves records
Answer: A pattern that deconstructs a record whose components are themselves records. Line(Point(var x1, var y1), Point(var x2, var y2)) deconstructs nested records all the way down.
Can you use var for component bindings in a record pattern?
- No
- Yes — Point(var x, var y) infers each component type
- Only for the outer record
- Only in instanceof
Answer: Yes — Point(var x, var y) infers each component type. var is allowed for component bindings; the compiler infers each component's type.
Why do record patterns pair so well with sealed types?
- A switch can deconstruct and stay exhaustive with no default
- They run faster
- They reduce memory
- They allow inheritance
Answer: A switch can deconstruct and stay exhaustive with no default. Over a sealed hierarchy of records, a switch can deconstruct each case and the compiler proves exhaustiveness.
Without record patterns, how would you read a matched record's data?
- You couldn't
- Use reflection
- Bind the whole record, then call its accessor methods
- Cast to Object
Answer: Bind the whole record, then call its accessor methods. Previously you bound the record (case Point p) and called p.x(), p.y() yourself.
Can a record pattern include a guard with when?
- No
- Yes — case Pair(Integer a, Integer b) when a.equals(b) -> ...
- Only in instanceof
- Only without nesting
Answer: Yes — case Pair(Integer a, Integer b) when a.equals(b) -> .... Record patterns combine with when guards just like type patterns.
Do the component types in a record pattern have to exactly match the record's declared types?
- They can be anything
- They must be Object
- They must be primitives
- They must match or be valid sub-patterns
Answer: They must match or be valid sub-patterns. Each sub-pattern must be compatible with the record component's type (the same type, var, or a nested pattern).
What is the main readability benefit of record patterns?
- Shorter class names
- Deconstruct data inline instead of repeated accessor calls
- Automatic null checks
- Faster compilation
Answer: Deconstruct data inline instead of repeated accessor calls. You name the pieces you need right in the pattern, avoiding a chain of p.a().b() accessor calls.
Combining sealed + records + record patterns in a switch is often called...
- Reflection
- Lazy evaluation
- Data-oriented programming
- Generics
Answer: Data-oriented programming. Sealed types, records, and patterns together enable a data-oriented programming style.
Continue this course
- Previous: Sealed Classes
- Next: Method References — The four kinds of :: references and when each replaces a lambda
- Quick reference: Java cheat sheet
Frequently asked questions
What exactly is a record pattern?
A record pattern matches a value against a record type and, in the same step, deconstructs it into its components, binding each to a variable. Point(int x, int y) matches when the value is a Point and gives you x and y directly. It is the natural extension of type patterns: a type pattern binds the whole object (case Point p), while a record pattern binds the pieces inside it (case Point(int x, int y)). Record patterns were finalized in Java 21.
How do nested record patterns work?
Because a record's components can themselves be records, you can nest patterns to deconstruct an entire structure at once. Line(Point(var x1, var y1), Point(var x2, var y2)) matches a Line and reaches all the way down to the four coordinates in one pattern. This replaces a tedious chain like line.start().x() with named variables right where you need them, and it reads top-to-bottom like the data's own shape.
Can I use var inside a record pattern?
Yes. You can write the explicit component type — Point(int x, int y) — or let the compiler infer it with var — Point(var x, var y). var is especially convenient in deeply nested patterns where spelling out every type would be noisy. The inferred type is exactly the record component's declared type, just as with local-variable var.
Why are record patterns so powerful with sealed types?
A sealed interface gives you a closed set of record subtypes, and a switch can have one record-pattern arm per subtype. The compiler then proves the switch is exhaustive — no default needed — and each arm deconstructs its record inline. This combination of sealed + records + record patterns is the heart of 'data-oriented programming' in modern Java: you model data as a closed family of records and process it with a single, fully checked switch.
Do record patterns work with guards and null?
Yes. A record pattern can carry a when guard, e.g. case Pair(Integer a, Integer b) when a.equals(b) -> ..., and the bound components are visible to both the guard and the arm body. As for null: a record pattern only matches a non-null value of the record type, so a null selector falls through to a case null (in a switch) or simply doesn't match (in instanceof). Note that the components themselves can still be null and will be bound as such.