Back to Blog
Java

Java Record Pattern Matching in Practice

java record pattern matching: Learn how Java record patterns deconstruct records in instanceof and switch, with nested patterns, guards, and null handling.

java recordspattern matchingswitch expressionsinstanceofrecord deconstruction
Illustration of a Java record being split into its component variables by a pattern matching operation.

Record patterns let you deconstruct a record instance directly inside a pattern, binding its components to variables. Combined with instanceof and switch, java record pattern matching removes the boilerplate of manual accessor calls and explicit type checks. The feature was finalized in Java 21, so the examples here assume a Java 21 compiler and runtime.

What Record Patterns Add to Pattern Matching

Pattern matching in Java started with type patterns for instanceof, then extended to switch. Record patterns take that further by matching the structure of a record, not just its type. When a pattern matches, each record component is bound to a new variable in a single step.

record Point(int x, int y) {}

Without record patterns, extracting the components requires a cast and accessor calls:

if (obj instanceof Point p) { int sum = p.x() + p.y(); }

With a record pattern, the same logic binds the components directly:

if (obj instanceof Point(int x, int y)) { int sum = x + y; }

The compiler checks that the pattern matches the record's shape. If Point has two int components, the pattern must declare two variables of compatible types. A mismatch is a compile error, not a runtime surprise.

Using instanceof with Record Patterns

The instanceof form is the simplest entry point. It combines a type test, a cast, and component extraction into one expression.

public int distanceFromOrigin(Object obj) { if (obj instanceof Point(int x, int y)) { return x * x + y * y; } return -1; }

If obj is not a Point, the pattern fails and the block is skipped. If obj is null, instanceof returns false, so the null case never reaches the pattern. This makes instanceof record patterns safe for inputs that may be null.

The variables x and y are scoped to the if block, which keeps the code concise without leaking temporary variables. For a single record this is already an improvement, but the real value appears when the record is nested inside another structure.

Switch Expressions and Record Patterns

Switch pattern matching, finalized in Java 21, lets a switch statement or expression match against record patterns directly. This is useful when a method receives an object that could be one of several record types.

public String describe(Object obj) { return switch (obj) { case Point(int x, int y) -> "point at " + x + ", " + y; case Circle(double radius) -> "circle with radius " + radius; case null -> "null input"; default -> "unknown type"; }; }

The switch evaluates the patterns in order and picks the first match. Because records have a fixed component list, the compiler can verify that the pattern's variable types are compatible with the record's declared component types.

A switch expression using record patterns must be exhaustive when used as an expression. If the possible types are not fully covered, add a default branch. The null case is separate: a switch over a null value throws NullPointerException unless a null case is present, so include one when null is a realistic input.

Nested Record Patterns for Deeper Structures

Record patterns compose. A pattern can match a record whose components are themselves records, and the nested patterns bind the inner components in one step.

record Line(Point start, Point end) {}

Matching a Line and extracting both endpoints' coordinates requires a nested pattern:

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

The outer pattern matches Line, then each component pattern matches Point and binds its int components. If any level fails, the whole pattern fails. This is significantly shorter than the equivalent chain of instanceof checks and casts, and it keeps the structure of the data visible in the code.

Nested patterns work in switch too, so a switch can dispatch on a multi-level structure without writing helper methods for each level.

Guarded Patterns and Null Handling

A pattern match can be refined with a when clause, which acts as a guard evaluated only after the pattern matches.

public String classify(Object obj) { return switch (obj) { case Point(int x, int y) when x > 0 && y > 0 -> "positive quadrant"; case Point(int x, int y) -> "other quadrant"; case null -> "null"; default -> "not a point"; }; }

The guard runs only when the pattern itself matches. If the guard returns false, the switch continues to the next case. This is cleaner than nesting an if inside the case body, and it keeps all the conditions in the switch structure.

Null handling differs between the two forms. instanceof returns false for null, so no null case is needed. Switch pattern matching, however, throws NullPointerException for a null selector unless a null case is present. When a switch can receive null, add an explicit case null branch before the record patterns.

Runtime Behavior and Performance Considerations

Record patterns are a compile-time feature; they do not rely on reflection at the point of matching. The compiler generates the same kind of type checks and accessor calls you would write by hand, so the runtime cost is comparable to a manual instanceof plus cast plus accessor invocation.

The record accessors used by the pattern are the final methods generated for each component. Because they are final, the JIT can inline them in hot paths, and the type check is a standard instanceof test. There is no hidden allocation when a pattern binds components; the variables are ordinary local variables.

One practical point: pattern matching evaluates the guard after the pattern match, so a guard with side effects runs only when the pattern succeeds. Keep guards free of side effects anyway, because switch case order makes the evaluation order visible and harder to reason about.

Version and Compatibility Requirements

Record patterns were previewed in Java 19 and finalized in Java 21. Simple type patterns for instanceof have been available since Java 16, but the record deconstruction syntax requires a Java 21 compiler. The bytecode produced by a Java 21 compiler will not run on older runtimes, so the whole toolchain — compiler, build system, and runtime — must target Java 21 or later.

When migrating an existing codebase, record patterns can replace manual instanceof chains incrementally. The refactoring is local: each method can be updated independently, and the compiler will catch any pattern that no longer matches the record's shape. This makes the feature safe to adopt without a large-scale rewrite.

java record pattern matching: Practical Usage and Code Examp | RYUSLOG DEV