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?

Java - Pattern Matching

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 a switch or if 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 our Case class we need to remember to implement our own Object#equals method.
  • Writing the syntax for our predicates is cumbersome. This can be overcome by creating a simplified Predicate creation method that can be a static import. This could also be done for the Match and Case 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.