Java Record Patterns: Destructuring and Matching
java record patterns: Learn how to use Java record patterns to destructure records in pattern matching, including nested patterns, switch integration, and common pitfa...
Java record patterns, introduced as a preview in Java 19 and finalized in Java 21, let you destructure a record directly in pattern matching. Instead of extracting fields manually, you can bind them to variables in a single expression.
What Are Record Patterns?
Record patterns let you match a record type and bind its components to variables in one step. Before Java 21, extracting fields from a record required explicit accessor calls. With record patterns, you can write:
record Point(int x, int y) {} Object obj = new Point(3, 4); if (obj instanceof Point(int x, int y)) { System.out.println(x + ", " + y); }
The instanceof pattern binds x and y to the record's components. This works because the record's canonical constructor and accessor methods are implied. The pattern syntax mirrors the record declaration.
Basic Syntax and Compile-Time Checks
A record pattern uses the record's type name followed by a parenthesized list of patterns for each component. For example, Point(int x, int y) is a record pattern that matches any Point instance and binds its x and y fields.
The compiler verifies that the number of patterns matches the record's component count. If you write Point(int x) for a two-component record, you get a compile error. The component patterns can be any pattern, not just variable names. This is where record patterns become powerful.
Nested Record Patterns
Record patterns can be nested inside other record patterns. Consider a Line record that contains two Point instances:
record Line(Point start, Point end) {}
You can match a Line and extract the coordinates of both points in one expression:
if (obj instanceof Line(Point(int x1, int y1), Point(int x2, int y2))) { // x1, y1, x2, y2 are now in scope }
This eliminates the need for multiple instanceof checks or temporary variables. The pattern matching engine handles the nested destructuring automatically. The syntax is straightforward: each component pattern can itself be a record pattern.
Type Patterns and Null Handling
Record patterns are a special case of type patterns. A type pattern like String s matches any non-null value of type String and binds it to s. Record patterns extend this by also destructuring the record. However, both types of patterns require the matched value to be non-null. If obj is null, the instanceof check fails and the pattern does not match.
This is important because record patterns do not handle null records. If you need to treat null as a valid case—for example, in a switch—you must include a null case explicitly. The compiler enforces exhaustiveness in switch statements, but null handling is separate.
Using Record Patterns with Switch
Record patterns integrate with both switch statements and switch expressions. This is where they shine for polymorphic dispatch. Consider a sealed interface Shape with implementations Circle and Rectangle:
sealed interface Shape permits Circle, Rectangle {} record Circle(double radius) implements Shape {} record Rectangle(double width, double height) implements Shape {} double area(Shape shape) { return switch (shape) { case Circle(double r) -> Math.PI * r * r; case Rectangle(double w, double h) -> w * h; }; }
The switch expression uses record patterns to destructure each shape directly. Because Shape is sealed, the compiler knows all possible subtypes, so the switch is exhaustive without a default branch. This reduces boilerplate and makes the intent clear.
Guarded Patterns for Conditional Logic
Sometimes a record pattern alone is not enough. You may need to apply a condition to the bound variables. Guarded patterns, introduced with the when clause, allow you to add a boolean condition to a pattern. For example:
if (obj instanceof Point(int x, int y) when x > 0 && y > 0) { // Only matches points in the first quadrant }
The when clause is evaluated only after the pattern matches. If the condition is false, the pattern is considered not to match, and the else branch or next case is tried. This is particularly useful in switch expressions where you can combine multiple guarded cases.
Performance and Runtime Cost
Record patterns are resolved at compile time. The compiler generates code that performs type checks and casts, then invokes the record's accessor methods to extract components. There is no reflection involved, so the runtime cost is comparable to manual extraction. The JIT compiler can often inline these accessors, making the overhead negligible.
One subtlety: the order of evaluation in nested patterns is left-to-right. If a component pattern fails to match, the remaining components are not evaluated. This is consistent with short-circuit behavior and can be useful when a later component pattern has side effects—though you should avoid side effects in patterns anyway.
Compatibility and Migration
Record patterns require Java 21 or later. If you are on an earlier version, you can use the preview features in Java 19 and 20, but the syntax was refined. When migrating, note that the when clause replaced the earlier && syntax for guarded patterns. Code written for preview versions may need adjustment.
Also, record patterns work with any record, including local records and records defined inside methods. However, they do not work with classes that are not records. If you have a class with accessor methods, you cannot use record patterns directly; you would need to convert it to a record or use traditional type patterns.
Common Pitfalls and Limitations
One common mistake is assuming that record patterns can bind to fields of a non-record class. They cannot. Another is forgetting that pattern variables are only in scope when the pattern matches. In an if statement, the variables are available in the true branch, not in the else branch.
Another limitation: record patterns cannot be used in a standalone assignment. They only appear in instanceof or switch contexts. You cannot write Point(int x, int y) = point; to destructure outside of pattern matching.
Finally, be aware that record patterns do not perform any null checks on the record components themselves. If a component is null, it will be bound to the variable normally. If you need to ensure non-null components, you must add explicit checks.
This last point is often overlooked. The pattern only checks the record instance itself, not its components. For example, Point(null, 5) will match Point(int x, int y) and bind x to null, which may cause a NullPointerException later if you use x in an arithmetic expression. Guard against this by using type patterns for components that must be non-null, such as Point(String name, int age) with Point(String s, int a)—but that still allows s to be null. To enforce non-null, you need a guard like when s != null.