Command Palette

Search for a command to run...

PHASE 11Intermediate Java 21+ ~34 min· topic 3 of 6

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) inside Line(Point(var x, var y), Point p).
var pattern
var x in a component slot. It matches anything, including null, and gives x the 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> in Pair(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.

Main.javawhole filejava
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.

Main.javawhole filejava
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";
}
Nesting: look several levels deepdiagram
Rendering diagram…

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.

Main.javawhole filejava
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.

Main.javawhole filejava
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 == null

05Generic 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.

terminal
$ javac Main.java
── expected output ──
Main.java:7: error: the switch expression does not cover all possible input values
return switch (p) {
^
1 error

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).

terminal
$ javac --release 17 Main.java
── expected output ──
Main.java:5: error: deconstruction patterns are not supported in -source 17
if (o instanceof Point(long x, int y)) System.out.println(x);
^
(use -source 21 or higher to enable deconstruction patterns)
1 error

Try it yourself

  1. 1

    Add a rule

    In the expression example, add a simplification case Add(Expr x, Num(int v)) when v == 0 -> simplify(x); and build new Add(new Num(5), new Num(0)). Predict where to put the new case so it isn't dominated, then run.

  2. 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. 3

    Swap var for a type

    In the null example, change case Box(var other) to case Box(Double d). Predict which inputs now fall through to default, then check.

Code & diagrams

Evaluate and simplify an expression tree Java 21+ New tab
This example needs Java 21+. The in-browser compiler is Java 17: install JDK 21 or newer and run it with "java Main.java".

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]
Record patterns and null components Java 21+ New tab
This example needs Java 21+. The in-browser compiler is Java 17: install JDK 21 or newer and run it with "java Main.java".

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?         false
Generic records, inference and nested exhaustiveness Java 21+ New tab
This example needs Java 21+. The in-browser compiler is Java 17: install JDK 21 or newer and run it with "java Main.java".

Expected output

Asha & Ravi
Asha alone
Ravi alone
nobody
Pair[first=2, second=left]
Not allowed: record patterns in a for headerjava
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)).

terminal
$ javac Main.java
── what you'll see ──
Main.java:5: error: incorrect number of nested patterns
if (o instanceof Point(int x)) System.out.println(x);
^
required: int,int
found: int
1 error

Break #2

A component type that doesn't fit

Write o instanceof Point(long x, int y) for record Point(int x, int y).

terminal
$ javac Main.java
── what you'll see ──
Main.java:5: error: incompatible types: pattern of type long is not applicable at int
if (o instanceof Point(long x, int y)) System.out.println(x);
^
1 error

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.

terminal
$ javac Main.java
── what you'll see ──
Main.java:7: error: the switch expression does not cover all possible input values
return switch (p) {
^
1 error

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 when never 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. 1

    Topic 11.1's type pattern o instanceof Point p tells you the type, but you still call p.x() and p.y(). A record pattern o instanceof Point(int x, int y) matches when o is a non-null Point and binds each component: x gets p.x(), y gets p.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. 2

    Each component slot holds a nested pattern: a type pattern (int x, String s), var (infers the component type), or another record pattern. So Line(Point(var x1, var y1), Point(var x2, var y2)) matches a Line whose two components are Points 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. 3

    null rules. A record pattern never matches null itself. Inside it, a nested type pattern that covers the whole component type (like Object x for an Object component, or var v) does match a null component, while a narrower one (like String s for an Object component) doesn't, and neither does a nested record pattern. So Box(var v) matches new Box(null) with v == null, but Box(String s) doesn't.

  4. 4

    Record patterns work with generic records, and the compiler infers type arguments: if (o instanceof Pair(var a, var b)) works without writing Pair<String, Integer>. Combined with sealed interfaces (Topic 5.11) in a switch (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)) and Pair(None(), None()) together cover every pair with no default.

  5. 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. 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

01

What does o instanceof Point(int x, int y) do, step by step?

02

How do record patterns behave with null, both for the record and for its components?

03

How does the compiler check exhaustiveness for nested record patterns over sealed types?

04

Why can't you write a record pattern for an ordinary class?

05

What is the relationship between records, sealed interfaces and record patterns?

Practice

01

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.

02

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.

03

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 instanceof and in a switch case.

  • Done when you can write nested record patterns with var.

  • Done when you can predict which patterns match a null component.

  • 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).