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:
- 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.
- 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 - 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: