Record Patterns in Java 19: What They Are and How to Use Them

Record Patterns in Java 19: What They Are and How to Use Them


Introduction

Pattern matching is a powerful feature that allows us to write concise and expressive code by testing values against different patterns and extracting relevant information. Java has been gradually introducing pattern matching capabilities since Java 14, and one of the latest additions is record patterns. 

Record patterns are a preview feature in Java 19 that enable us to match values against record types and bind variables to the corresponding components of the record. 

In this blog post, we will explore what record patterns are, how they work, and how we can use them in switch expressions, statements, and instanceof expressions. By the end of this post, you will have a better understanding of how record patterns can simplify your code and make it more readable.



Body


Record Patterns in Switch Expressions and Statements

We can use record patterns as case labels in switch expressions and statements to match values against different records and perform different actions accordingly. 

For example, given an interface named Shape with three implementing records Circle(double radius), Rectangle(double length double width), Triangle(double base double height), we can write a switch expression like this:

double area(Shape shape) {
    return switch (shape) {
        case Circle(var r) -> Math.PI * r * r;
        case Rectangle(var l var w) -> l * w;
        case Triangle(var b var h) -> b * h / 2;
    };
}

In this switch expression, each case label uses a named record pattern to match against a specific shape and bind variables to its components. Then it uses these variables to calculate the area of that shape.

We can also use constants guards nulls default cases etc with record patterns in switch expressions and statements. 
For example:

String describe(Point point) {
    return switch (point) {
        case Point(0 0) -> "Origin";
        case Point(var x 0) p -> "On the X-axis at " + p;
        case Point(0 var y) p -> "On the Y-axis at " + p;
        case Point(var x var y) && x == y -> "On the diagonal line y = x";
        case Point(var x var y) && x == -y -> "On the diagonal line y = -x";
        case null -> "Null point";
        default -> "Somewhere else";
    };
}

In this switch statement, we use constants to match specific points guards to add additional conditions nulls to handle null values and default cases to cover all other possibilities. We also use named record patterns to refer to the whole point variable when needed.

The compiler checks for exhaustiveness when we use record patterns in switch expressions and statements. That means it will warn us if we forget to cover some cases or if some cases are unreachable. For example, if we omit the default case in the previous switch statement, the compiler will complain that the switch expression is not exhaustive.


Record Patterns in Instanceof Expressions

We can use record patterns as operands of instanceof expressions to test if an object is an instance of a record type and bind variables to its components. 
For example:

Object obj = new Point(1 2);
if (obj instanceof Point(var x var y)) {
    System.out.println("The point has coordinates (" + x + ", " + y + ")");
}

This code snippet, we use an unnamed record pattern to check if obj is a Point object and bind its components to x and y variables. Then we use these variables inside the if block.

We can also use logical operators (&& || !) with instanceof and record patterns to combine multiple tests. 
For example:

Object obj = new Pair<>("Hello" 42);
if (obj instanceof Pair<String Integer> || obj instanceof Pair<Integer String>) {
    System.out.println("The pair contains a string and an integer");
}

In code snippet, we use two generic record patterns with explicit type arguments and combine them with || operator to check if obj is a Pair object that contains a string and an integer in any order.

When we use instanceof with record patterns, the compiler performs type narrowing on the tested object. That means it will treat it as having the type of the matched record within the scope of the test. For example:

Object obj = new Circle(5);
if (obj instanceof Circle c && c.radius() > 0) {
    System.out.println("The circle has positive radius");
}
System.out.println(obj.radius()); // compile-time error: Object does not have radius() method

In this code snippet, we use a named record pattern with an identifier c to test if obj is a Circle object and bind it to c variable. Then we use c.radius() method inside the if block because c has been narrowed down to Circle type. 

However, outside the if block obj still has Object type so calling obj.radius() method will cause a compile-time error.


Some questions to test your understanding.

  • What is a record pattern and what does it allow us to do?

  • What is the difference between a named and an unnamed record pattern?

  • How can we use record patterns as case labels in switch expressions and statements?

  • How can we use constants guards nulls default cases etc with record patterns in switch expressions and statements?

  • How does the compiler check for exhaustiveness when we use record patterns in switch expressions and statements?

  • How can we use record patterns as operands of instanceof expressions?

  • How can we use logical operators (&& || !) with instanceof and record patterns to combine multiple tests?

  • How does the compiler perform type narrowing on the tested object when we use instanceof with record patterns?


Answers 

  • A record pattern is a construct that allows us to match values against a record type and bind variables to the corresponding components of the record. We can also give the record pattern an optional identifier, which makes it a named record pattern and allows us to refer to the record pattern variable.

  • The difference between a named and an unnamed record pattern is that a named record pattern has an identifier that allows us to refer to the whole record variable, while an unnamed record pattern does not have an identifier and only allows us to refer to the component variables.

  • We can use record patterns as case labels in switch expressions and statements to match values against different records and perform different actions accordingly.

    For example, given an interface named Shape with three implementing records Circle(double radius), Rectangle(double length double width), Triangle(double base double height), we can write a switch expression like this:

double area(Shape shape) {
    return switch (shape) {
        case Circle(var r) -> Math.PI * r * r;
        case Rectangle(var l var w) -> l * w;
        case Triangle(var b var h) -> b * h / 2;
    };
}

  • In this switch expression, each case label uses a named record pattern to match against a specific shape and bind variables to its components. Then it uses these variables to calculate the area of that shape.

  • We can use constants guards nulls default cases etc with record patterns in switch expressions and statements.
    For example:

String describe(Point point) {
    return switch (point) {
        case Point(0 0) -> "Origin";
        case Point(var x 0) p -> "On the X-axis at " + p;
        case Point(0 var y) p -> "On the Y-axis at " + p;
        case Point(var x var y) && x == y -> "On the diagonal line y = x";
        case Point(var x var y) && x == -y -> "On the diagonal line y = -x";
        case null -> "Null point";
        default -> "Somewhere else";
    };
}

  • In this switch statement, we use constants to match specific points guards to add additional conditions nulls to handle null values and default cases to cover all other possibilities. We also use named record patterns to refer to the whole point variable when needed.

  • The compiler checks for exhaustiveness when we use record patterns in switch expressions and statements. That means it will warn us if we forget to cover some cases or if some cases are unreachable.
    For example, if we omit the default case in the previous switch statement, the compiler will complain that the switch expression is not exhaustive.

  • We can use record patterns as operands of instanceof expressions to test if an object is an instance of a record type and bind variables to its components.
    For example:

Object obj = new Point(1 2);
if (obj instanceof Point(var x var y)) {
    System.out.println("The point has coordinates (" + x + ", " + y + ")");
}

  • In this code snippet, we use an unnamed record pattern to check if obj is a Point object and bind its components to x and y variables. Then we use these variables inside the if block.

  • We can also use logical operators (&& || !) with instanceof and record patterns to combine multiple tests.
    For example:

Object obj = new Pair<>("Hello" 42);
if (obj instanceof Pair<String Integer> || obj instanceof Pair<Integer String>) {
    System.out.println("The pair contains a string and an integer");
}

  • In this code snippet, we use two generic record patterns with explicit type arguments and combine them with || operator to check if obj is a Pair object that contains a string and an integer in any order.

  • When we use instanceof with record patterns, the compiler performs type narrowing on the tested object. That means it will treat it as having the type of the matched record within the scope of the test.
    For example:

Object obj = new Circle(5);
if (obj instanceof Circle c && c.radius() > 0) {
    System.out.println("The circle has positive radius");
}
System.out.println(obj.radius()); // compile-time error: Object does not have radius() method

  • In this code snippet, we use a named record pattern with an identifier c to test if obj is a Circle object and bind it to c variable. Then we use c.radius() method inside the if block because c has been narrowed down to Circle type.
    However, outside the if block obj still has Object type so calling obj.radius() method will cause a compile-time error.



Conclusion


Record patterns are a preview feature in Java 19 that allow us to match values against record types and bind variables to their components. They are useful for writing concise and expressive code for pattern matching scenarios.

We learned how to use record patterns in switch expressions statements and instanceof expressions how they work syntactically and semantically how they interact with constants guards nulls default cases etc how they support generic types inference exhaustiveness checking type narrowing etc


Here are some possible references for further reading about record patterns in Java:

  1. What good are Record Patterns in Java? An example based on Spark’s Dataset API by Gavin Ray. This article explains what record patterns are and how they can be used to write concise and powerful pattern matching code with an example based on Spark’s Dataset API.

  2. Record Patterns in Java 19 | Baeldung by Ramesh Lingappa. This tutorial covers how to use record patterns in instanceof expressions switch expressions and statements with examples and code snippets.

    It also covers some of the syntactic and semantic aspects of record patterns such as constants guards nulls default cases generic types inference exhaustiveness checking type narrowing etc

  3. JEP 405: Record Patterns (Preview) - openjdk.org by Brian Goetz et al. This document is the official proposal for adding record patterns as a preview feature in Java 19.

    It describes the motivation design goals specification implementation testing compatibility risks alternatives etc of record patterns.

Related Article: 

Previous Post Next Post