Pattern matching and flow scoping in Java
JEP 394 brought pattern matching to the Java language as a final, non-preview, feature.
To be more precise, it brought a specific type of pattern matching, the type pattern, to a specific expression, the instanceof
.
Along with pattern matching, the JEP brought another new concept to the language: flow scoping.
We'll discuss them in this article.
Pattern matching for instanceof
(short introduction)
A common use of the instanceof
operator is in equals
method implementations.
Suppose a class Article
representing a blog article.
Here's one possible implementation of the equals
method:
@Override
public boolean equals(Object obj) {
if (obj instanceof Article) {
Article that = (Article) obj;
return Objects.equals(title, that.title)
&& Objects.equals(date, that.date);
} else {
return false;
}
}
Yes, there are many other ways to implement the method, bear with me a little. Note the following:
-
we've used the
instanceof
operator to check whether the object is in fact an Article; and -
we've immediately cast the object to an
Article
instance when test evaluates totrue
.
Since Java 16 (without the need of --enable-preview
) it is possible to do:
@Override
public boolean equals(Object obj) {
if (obj instanceof Article that) {
return Objects.equals(title, that.title)
&& Objects.equals(date, that.date);
} else {
return false;
}
}
Note that the instanceof
expression introduces the local variable that
.
The cast occurs implicitly.
This is an example of the pattern matching for instanceof
.
We could also do it without the if
statement:
@Override
public boolean equals(Object obj) {
return obj instanceof Article that
&& Objects.equals(title, that.title)
&& Objects.equals(date, that.date);
}
Note that the variable that
is accessible on the right side of the &&
operator.
In other words, the variable that
is in the scope of that operator.
This is an example of the flow scoping introduced by pattern matching for instanceof
.
The "traditional" scope
Before going into more detail about flow scoping, let's look at the "traditional" scope.
The following for
statement:
var result = new ArrayList<Future<Foo>>();
for (var element : elements) {
var future = submit(element);
result.add(future);
}
Introduces two local variables:
-
element
; and -
future
.
The two variables can only be accessed within the for
loop.
In other words, the scope of variables is restricted to the for
statement.
Therefore, the following code does not compile:
var result = new ArrayList<Future<Foo>>();
for (var element : elements) {
var future = submit(element);
result.add(future);
}
// compilation error
System.out.println(element);
The element
variable cannot be accessed outside the for
statement block.
This is the scope for local variables that we are used to as Java developers.
It is somewhat natural to see that the scope is restricted within the curly brackets.
Flow scoping
Flow scoping differs from the previous one in a few important ways. It is not the curly brackets that delimit the scope. It is the program's execution flow that delimits the scope instead.
Let's look at the following example:
static void printIfString(Object o) {
if (!(o instanceof String s)) {
return;
}
System.out.println(s);
}
public static void main(String[] args) {
printIfString("Hello");
printIfString(LocalDate.now());
printIfString("World!");
}
When executed the program prints:
Hello
World!
Let's focus on the printIfString
method.
As it says, it only prints the object when it is a String
instance.
The interesting thing is that the local variable s
can be accessed at the end of the method:
if (!(o instanceof String s)) {
return;
}
// 's' is in scope!
System.out.println(s);
Note that if the object o
is not a String, then we exit the method via an early return
.
In other words, if the flow of execution continues after the if
statement block, then the object is certainly a String instance.
Let’s look at a variation of the previous example:
static void printIfString(Object o) {
if (!(o instanceof String s)) {
// 's' is NOT in scope
return;
} else {
// 's' is in scope
System.out.println(s);
}
}
The else
block is executed if, and only if, the object is a String instance.
In this case, the variable s
can be accessed in the else
block.
Conclusion
Pattern matching brought a new kind of scope to the Java language: flow scoping.
In the article we saw that it is defined by the program's execution flow.
It is, therefore, different from the "traditional" scope in which it is restricted by a method body or a statement block.