Java record patterns refer to the mechanism of deconstructing record values such that they can be combined with pattern matching enabling more sophisticated data queries.
This tutorial will discuss the record patterns in detail along with how they help in pattern matching for instanceof
and switch
statements. The record patterns feature (completed) is part of the Java 21 release.
1. Quick Recap of Java Records
Records, introduced in Java 14, act as transparent carriers for immutable data. Conceptually similar to tuples, records are used in places where a class is created only to act as a plain data carrier.
In the following code snippet, the Shape is a record type. In runtime, JVM generates the following:
- An all-arguments constructor.
- The getter methods with a pattern
instance.x()
wherex
is the component name. - The
hashCode()
andequals()
methods. - The
toString()
method.
record Shape(String type, long unit){}
void main() {
Shape circle = new Shape("Circle", 10);
System.out.println(circle.type()); // "Circle"
System.out.println(circle.unit()); // 10
System.out.println(circle); //Shape[type=Circle, unit=10]
}
2. Record Patterns
2.1. What is a Pattern?
In general terms, a pattern is a combination of a predicate (or test) that can be applied to a target and a set of local variables, called pattern variables, that are assigned values extracted from the target only if the test is successful.
In the following example,
- instanceof operator is the predicate
- the shape is the target, and
s
is the pattern variable which is initialized only if the ‘shape instanceof Rectangle’ is true.
Shape shape = ...;
if (shape instanceof Shape s) {
// we can use s here
}
2.2. What is a Record Pattern?
When we combine the concept of patterns with record
types, it is called record patterns. A record pattern consists of a record class type and a (possibly empty) pattern list which is used to match against the corresponding target record.
The record pattern can also disaggregate an instance of a record into its components. For example, in the following code snippet, the type and unit are pattern variables.
record Shape(String type, long unit){}
void main() {
Shape circle = new Shape("Circle", 10);
if (circle instanceof Shape(String type, long unit)) {
System.out.println("Area of " + type + " is : " + Math.PI * Math.pow(unit, 2));
}
}
The program output:
Area of Circle is : 314.1592653589793
When the program is executed, notice how the values of components type and unit are automatically populated
The
null
value does not match any record pattern.
For reference, if we do not use the record patterns then the above code will be written as follows:
if (circle instanceof Shape shape) {
System.out.println("Area of " + shape.type() + " is : " + Math.PI * Math.pow(shape.unit(), 2));
}
Notice how the circle instance is first cast to shape and then we use the accessor method to get the component values. In the above code, the shape is restricted to the scope of instanceof block and does not provide any further value.
Using record patterns helps in avoiding such unnecessary casts, as well as, record components are directly accessed making the code more concise, clean and readable.
3. Pattern Matching
Pattern matching is referred to be the operation to take a type pattern (e.g. record pattern) and perform matching against multiple patterns. We can do pattern matching with conditional statements such as if-else
, instanceof
, switch
statements etc.
In the following example, target shape has been tested against multiple patterns in the case statements.
interface Shape {}
record Circle(double radius) implements Shape { }
record Square(double side) implements Shape { }
record Rectangle(double length, double width) implements Shape { }
void main() {
Shape shape = new Circle(10);
switch(shape) {
case Circle c:
System.out.println("The shape is Circle with area: " + Math.PI * c.radius() * c.radius());
break;
case Square s:
System.out.println("The shape is Square with area: " + s.side() * s.side());
break;
case Rectangle r:
System.out.println("The shape is Rectangle with area: + " + r.length() * r.width());
break;
default:
System.out.println("Unknown Shape");
break;
}
}
Pattern matching, when combines with record destructuring, enables us to write very powerful pattern expressions that are very clean, concise and readable.
In the following example, object destructuring extracts the record component values and directly populate them into the pattern variables. This removes the need for temporary pattern variables s
, r
or c
that is only purpose is to provide access to component values. Here we are directly getting the component values and using them.
switch(shape) {
case Circle(double radius):
System.out.println("The shape is Circle with area: " + Math.PI * radius * radius);
break;
case Square(double side):
System.out.println("The shape is Square with area: " + side * side);
break;
case Rectangle(double length, double width):
System.out.println("The shape is Rectangle with area: + " + length * width);
break;
default:
System.out.println("Unknown Shape");
break;
}
Clearly, the above code is more concise and readable.
4. Conclusion
This Java tutorial discusses two relatively newer features added to the language i.e. record patterns and pattern matching. The key to understanding these related terms is first understanding what a pattern is. Then we can combine the concept of patterns to record types, and finally go deeper into pattern matching with instanceof and switch expressions.
Happy Learning !!
Comments