Back to Blog
Java

Java Record Pattern: Destructuring and Matching

java record pattern: Learn how Java record patterns simplify destructuring records in instanceof and switch, with nested patterns and guards.

JavaRecord PatternsPattern MatchingJava 21DestructuringSwitch Expressions
A visual representation of a Java record being destructured into its components via a pattern matching arrow.

When you work with records in Java, extracting component values usually means calling accessor methods. The java record pattern feature, finalized in Java 21, lets you destructure a record directly inside a pattern match. Instead of writing explicit casts and accessor calls, you declare a pattern that binds the record components to variables. This reduces boilerplate and makes the intended structure of the data explicit.

Consider a simple record representing a point:

public record Point(int x, int y) {}

Before record patterns, checking whether an object is a Point and then reading its coordinates required an explicit cast:

if (obj instanceof Point p) { int x = p.x(); int y = p.y(); // use x and y }

With a record pattern, you can bind x and y directly in the instanceof check:

if (obj instanceof Point(int x, int y)) { // use x and y directly }

The pattern Point(int x, int y) matches any Point and binds its components to the new variables x and y. This is the core idea behind record patterns: they let you match the shape of a record and extract its parts in one step.

Basic Record Pattern with instanceof

The simplest form of a record pattern appears in an instanceof expression. The syntax mirrors the record declaration: the record type followed by a parenthesized list of component patterns. Each component pattern can be a type pattern, a var pattern, or another record pattern.

if (obj instanceof Point(int x, int y)) { System.out.println("x=" + x + ", y=" + y); }

Here int x and int y are type patterns that also declare new variables. If the component type is not important, you can use var:

if (obj instanceof Point(var x, var y)) { // x and y are inferred }

Using var is convenient when you only need the values without caring about their static type. The variables are effectively final within the branch, following the same scoping rules as pattern variables in instanceof.

The pattern matches only if the object is a Point and the component patterns also match. For primitive components like int, the type pattern always matches because the value is already an int. For reference components, you can nest further patterns to validate or extract deeper structure.

Using Record Patterns in Switch

Record patterns become more powerful when combined with switch statements and switch expressions. Java 21 allows pattern labels in switch, and record patterns can be used as case labels. This is especially useful when you have a sealed hierarchy of records and want to handle each variant cleanly.

Suppose you have a sealed interface for shapes with two record implementations:

sealed interface Shape permits Circle, Rectangle {} record Circle(double radius) implements Shape {} record Rectangle(double width, double height) implements Shape {}

You can write a switch expression that destructures each record directly:

static double area(Shape shape) { return switch (shape) { case Circle(double r) -> Math.PI * r * r; case Rectangle(double w, double h) -> w * h; }; }

Each case pattern binds the component variables. The switch expression is exhaustive because the sealed interface covers all possible subtypes. If you add a new record type later, the compiler will flag the missing case, which helps maintainability.

You can also use record patterns in a classic switch statement with case labels and a colon, but the arrow syntax is more concise and avoids fall-through. The pattern variables are scoped to the case block, so you don't need to declare them separately.

Nested Record Patterns

Record patterns can be nested to destructure records that contain other records. This is where the feature truly shines for complex data structures. For example, consider a record that represents a line segment:

record Line(Point start, Point end) {}

You can match a Line and extract the coordinates of both points in a single pattern:

if (obj instanceof Line(Point(int x1, int y1), Point(int x2, int y2))) { double length = Math.hypot(x2 - x1, y2 - y1); // use length }

The outer pattern Line(Point(...), Point(...)) matches only if the object is a Line and both components are Point instances. The inner patterns bind the coordinates. If either component is not a Point, the overall pattern fails, and the if block is skipped.

Nested patterns can also mix var with type patterns. For instance, if you only care about the start point's coordinates but not the end point's type, you could write:

if (obj instanceof Line(Point(int x1, int y1), var end)) { // x1, y1, and end are bound }

This flexibility lets you express exactly the level of detail you need without writing multiple nested instanceof checks and casts.

Adding Guards to Record Patterns

Pattern matching in Java supports guarded patterns using the when clause. A guard is a boolean expression that further restricts when a pattern matches. This is useful when you need to validate the extracted values before proceeding.

For example, you might want to handle only points that lie in the first quadrant:

if (obj instanceof Point(int x, int y) when x > 0 && y > 0) { System.out.println("First quadrant: (" + x + ", " + y + ")"); }

The guard is evaluated only after the pattern matches. If the guard evaluates to false, the pattern does not match, and the instanceof expression returns false. In a switch, a guarded case is skipped if the guard fails, and the switch continues to the next case.

Guards can reference the pattern variables, as shown above. They can also call methods or perform more complex checks. Keep guards free of side effects; they may be evaluated more than once in some control flow contexts, and relying on side effects makes the code harder to reason about.

Common Mistakes and Edge Cases

Record patterns look simple, but there are a few pitfalls worth understanding.

One mistake is assuming that a record pattern can match a record with a different number of components. The pattern must have exactly the same number of components as the record declaration. If you write Point(int x) for a two-component record, the code will not compile.

Another edge case involves generic records. If a record has type parameters, you must use the raw type or a wildcard in the pattern? Actually, record patterns work with generic records, but the type inference can be tricky. For example:

record Box<T>(T content) {}

You can match a Box with a type pattern:

if (obj instanceof Box<String>(String content)) { // content is a String }

The compiler checks that the obj is a Box<String> before the pattern is applied. If you use Box(var content), the type of content is inferred from the type of obj. If obj is a Box<Integer>, then content will be an Integer.

Record patterns also work with records that have components of array types. The component pattern can be an array type pattern, but you cannot destructure the array elements themselves. You would need to access the array normally after binding it.

Finally, remember that record patterns are not the same as record deconstruction in other languages. They are purely a pattern matching feature; they do not introduce new syntax for assignment or variable declaration outside of patterns.

Performance and Runtime Behavior

Record patterns are a compile-time feature. The compiler translates a record pattern into the equivalent sequence of type checks, casts, and accessor method calls. There is no reflection or runtime dispatch involved. This means the performance characteristics are identical to writing the manual instanceof and accessor calls yourself.

For example, the pattern Point(int x, int y) is compiled to:

  • an instanceof check against Point,
  • a cast to Point,
  • calls to x() and y().

No additional object allocation occurs, and the bytecode is as efficient as hand-written code. The main benefit is source code readability, not runtime speed.

One subtlety is that the order of component extraction is left to right, and if any component pattern fails, the remaining components are not evaluated. This is relevant when a component pattern has side effects, though side effects in patterns are discouraged.

In terms of memory, record patterns do not create copies of the record or its components. They simply bind variables to the values returned by the accessor methods. For primitive components, this is a direct value copy; for reference components, the variable refers to the same object.

Compatibility and Migration

Record patterns were introduced as a preview feature in Java 19 and became final in Java 21. To use them, you need a JDK that supports Java 21 or later. If you are on an earlier version, you cannot use record patterns without enabling preview features, and the syntax may differ slightly from the final version.

When migrating existing code, record patterns can replace verbose chains of instanceof and cast operations. The refactoring is usually straightforward: replace the type check and accessor calls with a single pattern. However, be aware that pattern variables are effectively final, so you cannot reassign them within the branch. If your existing code modifies the extracted value, you will need to introduce a new local variable.

Record patterns integrate with other pattern matching features like type patterns and guarded patterns. You can combine them freely in instanceof and switch. The compiler performs exhaustiveness checking for switch expressions over sealed hierarchies, which helps you catch missing cases at compile time.

If you are using libraries or frameworks that generate records, record patterns work with any record type as long as the record is accessible. There is no special interface or annotation required. This makes the feature broadly applicable across codebases that have adopted records.

For teams upgrading from Java 11 or 17, adopting record patterns is part of the broader shift to modern Java. It requires not only the language feature but also a runtime that supports it. In a production environment, ensure that all deployment targets run Java 21 or later before using record patterns in shared code.

java record pattern: Practical Usage and Code Examples | RYUSLOG DEV