Deep dive into Pattern Matching for Java
across other JVM languages and a variety of programming paradigms and design principles
Intro
On 14 Sep 2021, Oracle released production-ready JDK 17 as the latest long-term support (LTS) version. At the same time, they announced to change the ongoing LTS release cadence from three years to two years.
The most interesting new features since Java 11 are pattern matching, sealed classes and records, which are enhancements from Project Amber in order to increase developer productivity by evolving the Java language. Seal classes and records were already delivered as JEP 409 and JEP 395 (If you don’t know records and sealed classes, it would be nice if you check them). As JEP 395 stated below, they planned that two features would interoperate with pattern matching.
In addition to the combination of record classes and sealed classes, record classes lend themselves naturally to pattern matching. Because record classes couple their API to their state description, we will eventually be able to derive deconstruction patterns for record classes as well, and use sealed type information to determine exhaustiveness in
switch
expressions with type patterns or deconstruction patterns.
Pattern matching is not fully supported yet and JEP 420 (Pattern Matching for switch) and JEP 405 (Record Patterns & Array Patterns) are currently in progress.
As the person who was interested in functional programming, it was impressive for me to support these kinds of features in Java. I will explore what pattern matching of Java is, with comparing to other JVM languages, Scala and Kotlin, and looking through its meanings under a variety of programming paradigms and design principles.
- Pattern matching, comparing with JVM languages (Java, Kotlin, Scala)
- Meanings under programming paradigms and design principles
Pattern matching for Java
Motivation
Before comparing pattern matching between JVM languages, Let’s find out its necessity with an example code in Clean Code, Robert C. Martin.
You might feel uncomfortable with the above code, especially if you are an object-oriented programmer. However, I will try to figure out the problems without object-oriented approach at least for now. (You can see an object-oriented consideration in functional vs object-oriented
section)
The area
method repeats three processes: type test(check), type conversion(cast) and deconstruction(extract). This redundancy disturbs a developer to focus on business logic and makes the code error-prone and unsafe. Furthermore, the more types you test, the more boilerplate codes you get.
I will improve the code in two ways, which are newly introduced in Java.
- pattern matching for
instanceof
- pattern matching for
switch
JEP 394 : Pattern Matching for instanceof
Kotlin supports smart casts. When checking type with is
operator, the compiler explicit casts for immutable values and inserts casts automatically and safely.
Similarly, Java extended instanceof
operator to type patterns, which enables to perform instanceof
test, cast the target, and bind it to a pattern variable. I can refactor area
method with type patterns.
Through refactoring, I already achieved the improvement. But if you imagine the complex relationships and hierarchies between domain entities in real life, the nested if else
structure will appear redundant. Even though Java already supported switch
for a multi-armed equality test, switch
had a lot of limitations.
JEP 406 : Pattern Matching for switch (Preview)
Java 11 had only switch
statements, not expressions. It tests for exact equality against numeric, string, or enum constants. Currently you can use switch expressions feature which was delivered since Java 14.
A statement describes an action to be carried out, whereas an expression describes a value to be evaluated. It can be told that the part with statement is imperative and other part with expression is declarative. In above case, switch
expression makes the code more concise and safer.
In addition to switch
expressions, switch
was extended to support type patterns. It means that switch
can test and cast a type of object and bind it to a new variable through case
label. Finally, switch
can be applied to the example of Clean Code.
The code is much clearer and more understandable, as tidying up lots of repetitive code which obfuscates the more significant logic. Also, it is optimised in terms of performance. (Let # of types be N, O(N) in if else
to roughly constant time, O(1) in switch
)
Deep dive into the meanings
Functional vs Object-oriented
As every approach has tradeoffs, the above code improved with pattern matching is not an absolute solution. Interestingly, this topic is dealt with not only in Clean Code, which is base on an OOP, but also in Functional Programming Principles in Scala by Martin Odersky.
In Clean Code, the Procedural Shape, Snippet 1 is refactored to the below Polymorphic Shape, Snippet 3 having dynamic dispatch, which is the process of selecting which implementation of a method to call at run time.
Despite of refactoring, the author is not biased towards one of two types. From the perspective of OCP (Open Closed Principle) in design principles and design patterns, the author only pursues maximisation of extension and minimisation of modification while weighing procedural and OO codes according to use cases.
Procedural code (code using data structures) makes it easy to add new functions without changing the existing data structures. OO code, on the other hand, makes it easy to add new classes without changing existing functions.
Going further, in Functional Programming Principles in Scala, the procedure code is improved to functional decomposition with pattern matching as in Java, and comparing it with object-oriented decomposition.
Deconstruction Patterns
Truly someone who used to love Scala might not be surprised at pattern matching for Java, as the feature was already being supported as functional tendencies in Scala more than 10 years ago. Scala docs describes pattern matching as follows.
Pattern matching is a mechanism for checking a value against a pattern. A successful match can also deconstruct a value into its constituent parts. It is a more powerful version of the
switch
statement in Java and it can likewise be used in place of a series of if/else statements.
Previously we have already combined type check and cast of three processes: type test(check), type conversion(cast) and deconstruction(extract). As the docs mentioned above, a successful pattern matching should combine all the process including deconstruction. Snippet 2 of Java is refactored to Snippet 4 of Scala
Above the values(states) of case class
can be extracted through the deconstruction pattern along with the type pattern. In Java, JEP 405 (Record Patterns & Array Patterns) is being developed to deconstruct record values and array values. Pattern matching with the feature can be more powerful especially for more complicated object graphs.
It lets us decompose objects without worrying about null
or NullPointerException
. As above, nested record patterns can extract data from objects far more smoothly and concisely than traditional imperative code.
Conclusion
I hope that I will be able to utilise Java’s record patterns and array patterns as soon as possible. It is unlikely that the Java organization will support functional programming, but enabling the implementation of declarative code through record class and pattern matching will certainly provide additional options to programmers. I’m looking forward to not only the next LTS version, but also great synergies with the innovations from the Java camp like Project Loom and Graalvm.
Please feel free to comment on any mistakes and complements 🙌🏽