Going Beyond Java 8: Pattern Matching for instanceof
A developer and Java expert gives an overview of the instanceof operator in the recent versions of Java and how it can be used to improve your code.
Join the DZone community and get the full member experience.
Join For FreeIntroduction
According to some surveys, like JetBrains's great survey, Java 8 is currently the most used Java version, despite being a 2014 release.
This article is the first in a series of articles titled, "Going Beyond Java 8," inspired by the contents of my book "Java for Aliens." These articles will guide the reader step-by-step to explore the most important features introduced starting with Java 9. The aim is to make the reader aware of how important it is to move forward from Java 8, explaining the enormous advantages that the latest versions of the language offer.
In this article, we will look at an interesting novelty introduced in Java 14 as a preview feature (see related article), and definitively made official as a standard feature with Java 16. This is the first part of a complex feature known as pattern matching. It will affect various programming constructs in the future. This will radically change the way we use this operator.
The instanceof
Operator
The instanceof
binary operator, like comparison operators, returns a boolean value. The peculiarity of this operator is that it uses a reference as the first operand and a complex type as the second operand. It returns true
, if, at runtime, the reference (which defines the first operand) points to an object instantiated by the type (which defines the second operand). It returns true
, even if the reference points to an object instantiated by a subclass of the type specified with the second operand. If one of these two conditions is not met, the instanceof
operator returns false
.
Example
Suppose we want to create a system that establishes the salaries of employees of a company, considering the following classes:
public class Employee {
private String name;
private int salary;
private int number;
private String birthDate;
private String hireDate;
// setter and getter methods omitted
}
public class Programmer extends Employee {
private String knownLanguages;
private int yearsOfExperience;
// setter and getter methods omitted
}
public class Manager extends Employee {
private String workingTime;
// setter and getter methods omitted
}
public class SalesAgent extends Employee {
private String [] customerPortfolio;
private int commissions;
// setter and getter methods omitted
}
Suppose we have to pay the salaries to all types of employees, we could begin to group all the employees of the company within a heterogeneous collection:
xxxxxxxxxx
Employee [] arr = new Employee [180];
arr[0] = new Manager();
arr[1] = new Programmer();
arr[2] = new SalesAgent();
//... other assignments omitted
To manage employee salary, we could create the following method:
xxxxxxxxxx
public void payEmployee(Employee emp) {
if (emp instanceof Programmer) {
emp.setSalary(1500);
}
else if (emp instanceof Manager) {
emp.setSalary(3000);
}
else if (emp instanceof SalesAgent) {
emp.setSalary(1000);
}
System.out.println(emp.getClass().getName() + " - Salary = " + emp.getSalary());
//. . .
}
This uses the instanceof
operator to test which type of the polymorphic parameter emp
is pointing to and set the salary accordingly.
Now we can call this method within a foreach
loop (with 180 iterations), passing all of the elements of the heterogeneous collection, and thus reach our goal:
xxxxxxxxxx
//...
for (Employee employee : arr) {
payEmployee(employee);
//...
}
Object Cast
In the previous example, we observed that the instanceof
operator allows us to test to which type of instance a reference points. However, we already know that a superclass reference pointing to an object instantiated by a subclass cannot access the members declared in the subclass. For example, suppose a programmer's salary depends on the number of years of experience. In this situation, after testing the reference emp
points to a Programmer
instance, we will need to access the yearsOfExperience
variable. If we tried to access it using the syntax:
xxxxxxxxxx
emp.getYearsOfExperience();
we would get a compile-time error:
xxxxxxxxxx
error: cannot find symbol
emp.getYearsOfExperience();
^
symbol: method getYearsOfExperience()
location: variable emp of type Employee
In fact, the reference emp
, being of the Employee
type, will not be able to access the methods that are declared in the subclasses. To overcome this obstacle, we can use the object cast mechanism. In practice, we must declare a Programmer
type reference and make it point to the memory address where the reference emp
points, using the cast to confirm the pointing range. The new reference, being of the Programmer
type, will allow us to access any member of the Programmer
instance.
The cast of objects uses a syntax very similar to the cast between primitive data:
xxxxxxxxxx
if (emp instanceof Programmer) {
Programmer pro = (Programmer) emp;
if (pro.getYearsOfExperience() > 2)
//...
Note that we are now able to access the encapsulated variable yearsOfExperience
.
In Figure 1, we ideally outline the situation with a graphical representation, where the object is pointed to by the Employee
type emp
reference and the Programmer
type pro
reference.
Figure 1. Two different types of access to the same object
In Figure 1, the two references point to the same object, but with a different pointing range. It is essential to use the object cast only after checking its validity with the instanceof
operator. In fact, the compiler cannot determine if a certain object resides at a certain address instead of another. It is only at runtime that the Java Virtual Machine can use the instanceof
operator to resolve any doubts.
What's the Problem?
The combined use of the instanceof
operator and the object cast cannot be defined as a Java best practice, but it is certainly a well-known and widely used programming pattern. What is evident, however, is that this practice is undoubtedly verbose. It is also repetitive; in the previous example, we named the Programmer
type three times in two lines. Finally, after a test with instanceof
, the next statement is obviously assumed to be a cast. In fact, some IDEs allow us to automate this practice. However, if we do not automate the coding with an IDE, we may end up doing a lot of copy-and-pasting if this code is repeated several times, and this could also imply a ClassCastException
at runtime.
Pattern Matching for instanceof
Java 16 introduced the first part of a complex feature known as pattern matching. In this feature, a pattern (not to be confused with a design pattern) is composed of:
- a predicate: a test that looks for the matching of input with its operand.
- one or more pattern variables; these are extracted from the operand depending on the test result.
A pattern is therefore defined as a synthetic way of expressing a complex solution.
When this feature will be fully implemented, it will also affect other topics such as record
types, and the switch
construct. In version 15, pattern matching is still presented as a feature preview, and the only part implemented concerns the pattern that involves the instanceof
operator and the object cast that we have just seen. In practice, by enabling the feature preview, we can use an alternative syntax:
xxxxxxxxxx
if (emp instanceof Programmer pro) {
if (pro.getYearsOfExperience() > 2)
//...
}
//...
This syntax specifies a Programmer
-type reference, pro
, alongside the instanceof
operation. This reference can be used immediately, and no cast is required since it is already a Programmer
-type reference. The syntax is therefore very simple, but some observations must be made.
Pattern Scope
In particular, the pro
reference is defined as a pattern variable (up to Java 15 known as a binding variable). Pattern variables are particular local variables with a new type of constrained scope that depends on the pattern. Like a local variable, it can coexist with instance variables with the same identifier. The difference consists in the fact that its scope depends on the result of the predicate, i.e. if the result of the instanceof
test is true
or false
. In the previous example, the pro
reference was visible only in the code block relating to the if
construct. If we added the else
clause and prefixed the NOT operator to the instanceof
test in the following way:
xxxxxxxxxx
if (!emp instanceof Programmer pro) {
// the reference pro CANNOT be used here
} else {
// the reference pro can be used here!
}
then the pro
reference would be visible only in the code block of the else
clause, and not in the if
code block.
Pattern Constants?
Also, a pattern variable is implicitly final
, and its addressing cannot be changed. For example, the following
xxxxxxxxxx
if (emp instanceof Programmer pro) {
pro = new Programmer();
//...
}
would cause the below compile-time error:
xxxxxxxxxx
error: pattern binding pro may not be assigned
pro = new Programmer();
^
1 error
This was the behavior of pattern variables in Java 15, where pattern matching for instanceof
was still a feature preview. But in version 16 this feature has been formalized as a standard Java feature and this limitation has been permanently removed.
In any case, through a binding variable (since it is a reference) it is still possible to change the internal state of the object it points to. For example, the following code is valid:
xxxxxxxxxx
if (dip instanceof Programmer pro) {
pro.setYearsOfExperience(8);
}
In fact, a new address is not assigned to the pro
reference, but only a method is invoked on it.
Java 16
At present, the final evolution of pattern matching for instanceof
is not yet fully defined, and other changes could be made. When we get a chance to test the standard version of this feature, we will come back to update this article.
Conclusions
In this article, we have seen how Java 16 introduced pattern matching for instanceof
as an official feature. This new feature introduces a new type of scope for binding variables, and will forever change the way we use the instanceof
operator and object cast. In Java 15, this feature is still in preview, but in version 16, thanks to the feedback received from users, there will be changes.
The pattern matching for instanceof
, therefore, represents another small step forward for the language and is also a fundamental block for the pattern matching for switch
, introduced as a feature preview in Java 17.
Author Notes
Even ignoring the increased security offered by the latest versions of the JDK, there are plenty of reasons to upgrade your knowledge of Java, or at least your own Java runtime installations. My book "Java for Aliens," which inspired the "Going Beyond Java 8" series, contains all the information you need to learn Java from scratch, and uses a well-tested teaching method that has been perfected over 20 years of experience, which makes learning simple and exciting. It is also structured to deepen the topics and have superior knowledge that can make a difference to your career.
Published at DZone with permission of Claudio De Sio Cesari. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments