57. Introducing pattern matching
JDK 16 has introduced one of the major and complex features of Java, referred to as pattern matching. The future is wide open for this topic.
In a nutshell, pattern matching defines a synthetic expression for checking/testing whether a given variable has certain properties. If those properties are met, then automatically extract one or more parts of that variable into other variables. From this point forward, we can use those extracted variables.
A pattern matching instance (pay attention, this has nothing to do with design patterns) is a structure made of several components as follows (this is basically the pattern matching terminology):
- The target operand or the argument of the predicate: This is a variable (or an expression) that we aim to match.
- The predicate (or test): This is a check that takes place at runtime and aims to determine if the given target operand does or doesn’t have one or more properties (we match the target operand against the properties).
- One or more variables are referred to as pattern variables or binding variables: these variables are automatically extracted from the target operand if and only if the predicate/test succeeds.
- Finally, we have the pattern itself, which is represented by the predicate + binding variables.
Figure 2.31: Pattern matching components
So, we can say that Java pattern matching is a synthetic expression of a complex solution composed of four components: target operand, predicate/test, binding variable(s), and pattern = predicate + binding variable(s).
The scope of binding variables in pattern matching
The compiler decides the scope (visibility) of the binding variables, so we don’t have to bother with such aspects via special modifiers or other tricks. In the case of predicates that always pass (like an if(true) {}
), the compiler scopes the binding variables exactly as for the Java local variables.
But, most patterns make sense precisely because the predicate may fail. In such cases, the compiler applies a technique called flow scoping. That is actually a combination of the regular scoping and definitive assignment.
The definitive assignment is a technique used by the compiler based on the structure of statements and expressions to ensure that a local variable (or blank final
field) is definitely assigned before it is accessed by the code. In a pattern-matching context, a binding variable is assigned only if the predicate passes, so the definitive assignment aim is to find out the precise place when this is happening. Next, the regular block scope represents the code where the binding variable is in scope.
Do you want this as a simple important note? Here it is.
Important note
In pattern matching, the binding variable is flow-scoped. In other words, the scope of a binding variable covers only the block where the predicate passed.
We will cover this topic in Problem 59.
Guarded patterns
So far, we know that a pattern relies on a predicate/test for deciding whether the binding variables should be extracted from the target operand or not. In addition, sometimes we need to refine this predicate by appending to it extra boolean
checks based on the extracted binding variables. We name this a guarded pattern. In other words, if the predicate evaluates to true
, then the binding variables are extracted and they enter in further boolean
checks. If these checks are evaluated to true
, we can say that the target operand matches this guarded pattern.
We cover this in Problem 64.
Type coverage
In a nutshell, the switch
expressions and switch
statements that use null
and/or pattern labels should be exhaustive. In other words, we must cover all the possible values with switch case
labels.
We cover this in Problem 66.
Current status of pattern matching
Currently, Java supports type pattern matching for instanceof
and switch
, and record pattern-destructuring patterns for records (covered in Chapter 4). These are the final releases in JDK 21.