Java Nested Record Patterns Explained
java nested record patterns: Learn how Java nested record patterns let you deconstruct complex data structures safely and concisely in pattern matching.
Java nested record patterns let you deconstruct a record and its nested records in a single pattern, reducing the amount of boilerplate needed to inspect complex data structures. This article explains the syntax, shows practical examples, and covers the limitations you'll encounter when using them.
What Nested Record Patterns Solve
Before record patterns, extracting fields from a record meant calling accessor methods and manually checking types. With nested records, that quickly becomes repetitive and error-prone. Consider a Point record inside a Shape record. To get the coordinates of a Circle shape, you'd write multiple lines of code, each with its own null check or cast. Nested record patterns collapse that into one declarative pattern that the compiler verifies.
Record Patterns and Deconstruction Basics
A record pattern matches a record type and binds its components to variables. For a record Point(int x, int y), a pattern looks like Point(int x, int y). When used with instanceof, it both checks the type and extracts the fields in one step:
record Point(int x, int y) {} if (obj instanceof Point(int x, int y)) { System.out.println(x + ", " + y); }
The variables x and y are in scope only inside the if block. This is the foundation for nesting.
Nesting Patterns Inside Other Patterns
A record pattern can appear inside another record pattern. For example, if a Circle record contains a Point center and a radius:
record Circle(Point center, int radius) {}
You can match a Circle and deconstruct its center in the same pattern:
if (obj instanceof Circle(Point(int x, int y), int r)) { System.out.println("Center: " + x + "," + y + " radius: " + r); }
The pattern Circle(Point(int x, int y), int r) matches only when obj is a Circle and its center is a Point. If center is null, the pattern fails to match because the nested pattern requires a non-null Point. This behavior is consistent with how record patterns handle null: the top-level pattern also fails if the object is null.
Using Nested Record Patterns in Switch
Nested record patterns are especially useful in switch expressions and statements. The switch can select on the type and deconstruct nested data directly in each case:
record Point(int x, int y) {} record Line(Point start, Point end) {} record Circle(Point center, int radius) {} String describe(Object shape) { return switch (shape) { case Line(Point(int x1, int y1), Point(int x2, int y2)) -> "Line from (" + x1 + "," + y1 + ") to (" + x2 + "," + y2 + ")"; case Circle(Point(int x, int y), int r) -> "Circle at (" + x + "," + y + ") radius " + r; default -> "Unknown shape"; }; }
This is more readable than a chain of instanceof checks and explicit casts. The compiler also checks that the pattern is exhaustive when used in a switch expression, so you must provide a default or cover all sealed subtypes.
Guarded Patterns and Conditions
You can add a guard to a nested record pattern using when. This allows you to apply additional conditions after the pattern matches. For example, to match only circles with a positive radius:
case Circle(Point(int x, int y), int r) when r > 0 -> "Circle at (" + x + "," + y + ") with positive radius";
The guard is evaluated only after the pattern matches successfully. If the guard fails, the switch continues to the next case. This keeps the logic inline and avoids separate if statements inside the case body.
Common Pitfalls and Limitations
Nested record patterns have a few constraints worth knowing. First, the nested pattern must match exactly the record component type. If a component is an interface, you cannot use a record pattern directly on it without a type test. For example, if Circle had a Shape center instead of Point, you'd need a type pattern first: Circle(Shape s, int r) and then further test s.
Second, record patterns do not support wildcard binding for all components. You can use an unnamed variable _ (Java 22+) or a type pattern like Circle(Point p, int r) to bind the whole nested record. But you cannot skip a component without binding it. This is a deliberate design to keep the pattern syntax explicit.
Third, null handling is strict. A record pattern never matches a null reference. If your data can contain nulls, you need to handle that explicitly, either with a null case in a switch or by checking before the pattern match.
Performance and Maintainability Considerations
Record patterns are a compile-time feature. They do not add runtime overhead beyond the equivalent manual type checks and casts. The JVM does not allocate extra objects for the deconstructed fields; the variables are bound directly to the record components. This means you can use nested patterns freely in performance-sensitive code without worrying about hidden allocations.
From a maintainability perspective, nested record patterns make the shape of the data visible at the point of use. When a record's structure changes, the compiler flags every pattern that no longer matches, which is safer than relying on reflective access or manual casting. However, deeply nested patterns can become hard to read. If you find yourself matching three or four levels deep, consider extracting a helper method or using a sealed interface hierarchy to simplify the pattern.
A practical balance is to use nested patterns for two or three levels of nesting, and to rely on guards for additional conditions rather than adding more nested patterns. This keeps the code readable while still taking advantage of the concise deconstruction that Java nested record patterns provide.