Java - Pattern Matching
Scala's Pattern Matching is very cool. How close can we get to it's terse syntax with some simple Java code?
Scala's pattern matching is an extremely powerful tool, and one we as Java developers should all be jealous of. It builds on Java's switch
statement, taking it to the logical extreme: Matching against nearly any pattern you can imagine.
def matchTest(x: Int): String = x match {
case 1 => "one"
case 2 => "two"
case _ => "many"
}
The above code taken from Scala's amazing docs
The _
is the wildcard character, effectively the "default" of the match. While the above case may have you crying "That could be easily done using a switch!" let's look at another example to see more of the power of pattern matching.
Let's assume we have an abstract class with a number of concrete implementations. Using Scala's pattern matching you can easily handle this scenario:
def showNotification(notification: Notification): String = {
notification match {
case Email(email, title, _) =>
s"You got an email from $email with title: $title"
case SMS(number, message) =>
s"You got an SMS from $number! Message: $message"
}
}
The above code also taken from Scala's incredible docs
But Java's switch statements cannot, due to their limitation of requiring only a limited number of types (char, byte, short, int, Character, Byte, Short, Integer, String, and Enums). "Ah," you might say, "But that is what if statements are for!" You are, of course, not wrong. Java can handle this using chained if statements. But wouldn't it be nice to handle it without all the ceremony and line clutter of switches and ifs? Let's build out an emulation of Scala's pattern matching in Java!
We'll start by identifying a basic idea of what might actually be happening in Scala1. Let's look at the code again:
x match {
case 1 => "one"
case 2 => "two"
case _ => "many"
}
So the first thing we notice is that match
returns a value, in this case a String. Additionally, match supports 1 or more case
statements that function as short-circuit switches, returning the associated value. The variable we're matching to (in this case x
) then proceeds down the line of cases as they appear in the list. If it matches one of the cases, it short-circuits and returns the value. Otherwise, it hits the _
(or wildcard) case if one exists (and for the sake of brevity it also throws an exception if there is no wildcard case nor a matching case). Great, let's translate that into Java concepts we can understand.
Ignoring the match
keyword for a moment, the case
statements are all basic Tuples
. The left side of each tuple
represents a predicate
, and the right side a result value. The case
statements are iterated sequentially, and we have a final wildcard statement that is effectively a tuple
where the left side's predicate
always returns true
.
A basic version of a case
class might look like the following:
public class Case<O, N> {
private final Predicate<O> matchFunc;
private final N result;
private Case(Predicate<O> matchFunc, N result) {
this.matchFunc = Objects.requireNonNull(matchFunc);
this.result = Objects.requireNonNull(result);
}
Predicate<O> getMatchFunc() {
return matchFunc;
}
N getResult() {
return result;
}
//STATIC HELPER METHODS
public static <O, N> Case<O, N> of(
Predicate<O> matchFunc,
N result) {
return new Case<>(matchFunc, result);
}
}
Great, we now have a Case
class that we can instantiate from a static method to avoid the new
syntax. This class allows us to satisfy all of the functions of a case
statement that we identified, though how it handles wildcards isn't particularly pretty. We'll come back to address that concern later though, let's focus on getting our Match
class built first.
The match
keyword is similarly easy to understand. Given a variable it handles holding and iterating over the case
statements, and also has to handle errors when there is a missing case
clause. Here's a simple Match class we can use:
public class Match<O> {
private final O val;
private Match(O val) {
this.val = val;
}
public static <O> Match<O> match(O val) {
return new Match<>(val);
}
@SafeVarargs
public final <N> N to(Case<O, N>... cases) {
return Arrays
.stream(cases)
.filter(c -> c.getMatchFunc().test(val))
.findFirst()
.orElseThrow(NoSuchMatchException::new)
.getResult();
}
}
Wonderful, this class will handle the basics of what we care about. We use Match#match
to create a Match
instance and chain that together with a Match::to
statement, passing in an array of Case
objects. Those objects are filtered to return only those that match and we then grab the first in the list (which should be the first correct match from the array
of Case
objects). If there is no Case
object, we throw a simple wrapper RuntimeException
we've named NoSuchMatchException
, otherwise we return the result object from the Case
.
So what does this look like in action? Let's rebuild the first Scala example in both a traditional Java switch
and our new Match
syntax:
Integer x = new Random().nextInt(10);
String result;
switch (x) {
case 1:
result = "one";
break;
case 2:
result = "two";
break;
default:
result = "many";
}
result = Match.match(x).to(
Case.of(v -> v == 1, "one"),
Case.of(v -> v == 2, "two"),
Case.of(v -> true, "many")
);
There we go! Our new Match
syntax reduces the lines of code we've written from 10 to 5, that's a nice improvement! Additionally, the syntax is much more terse, and we no longer have to pre-declare result
! This implementation is fully functional, and as long you always remember to end with the wildcard Case
there isn't a huge need to change it. But what else can we do to add additional functionality? Let's start by giving ourselves some protection against an early wildcard.
To accomplish our goal we'll start by creating an internal class in Case
called DefaultCase
:
public class Case<O, N> {
private final Predicate<O> matchFunc;
private final N result;
private Case(Predicate<O> matchFunc, N result) {
this.matchFunc = Objects.requireNonNull(matchFunc);
this.result = Objects.requireNonNull(result);
}
Predicate<O> getMatchFunc() {
return matchFunc;
}
N getResult() {
return result;
}
//STATIC HELPER METHODS
public static <O, N> Case<O, N> of(
Predicate<O> matchFunc,
N result) {
return new Case<>(matchFunc, result);
}
public static <O, N> Case<O, N> wildcard(N result) {
return new DefaultCase<>((v) -> true, result);
}
//Package-Private class
static class DefaultCase<O, N> extends Case<O, N> {
public DefaultCase(Predicate<O> matchFunc, N result) {
super(matchFunc, result);
}
}
}
You'll notice we also added the Case#wildcard
method. We'll use this to allow a user to easily create a wildcard Case
in a clear and concise manor, removing the need to declare the matching Predicate
at all. But this change alone isn't enough. We also need to adjust our Match object.
public class Match<O> {
private final O val;
private Match(O val) {
this.val = val;
}
public static <O> Match<O> match(O val) {
return new Match<>(val);
}
@SafeVarargs
public final <N> N to(Case<O, N>... cases) {
return Arrays
.stream(cases)
.filter(c -> c.getMatchFunc().test(val))
.sorted((c1, c2) -> {
Boolean c1Default = c1 instanceof Case.DefaultCase;
Boolean c2Default = c2 instanceof Case.DefaultCase;
if (c1Default && c2Default) {
return 0;
} else if (c1Default) {
return 1;
} else if (c2Default) {
return -1;
} else {
return 0;
}
})
.findFirst()
.orElseThrow(NoSuchMatchException::new)
.getResult();
}
}
You'll notice the change straight away I imagine. We've added a Stream#sorted
call into our work flow which filters DefaultCase
objects to the bottom of the stream and Case
objects to the top. If both of the Case
objects are DefaultCase
, or if neither are, we don't change the order of the elements. Now we can declare the wildcard case at the start, the end, or in the middle of our Match#to
statement as long as we use Case#wildcard
!
Now we have a fully functional Match
class, but it does have it's limitations:
- Performance wise our
Match
class will never perform as well as aswitch
orif
block, but that shouldn't suprise anyone. - Unlike a
switch
block, we can't do cascading effects (One switch falling into another) - Scala has built in support for "Case classes", which are similar to Kotlin's "data class" and an Java POJO with only
public
final
variables. However, unlike Java's POJOs, Case classes equality check automatically looks at content and not reference. As a result, when building predicates for ourCase
class we need to remember to implement our ownObject#equals
method. - Writing the syntax for our
predicates
is cumbersome. This can be overcome by creating a simplifiedPredicate
creation method that can be a static import. This could also be done for theMatch
andCase
classes to simply creation.
All in all, our Match
and Case
classes are a servicable way to add basic Pattern Matching to Java. Hopefully this article gave you a better idea of how to basics of pattern matching works, how it can be helpful, and why you would want to use it in your own coding. To see a fuller implementation, I highly recommend looking at Vavr.
About the author
Photo by Tim Arterbury on Unsplash
1: Truth be told, Scala actually compiles match blocks into switch or if blocks, depending on what is more optimized.