Java 16 Pattern Matching Fun


Java 16 brings Pattern Matching for instanceof. It’s a feature with exciting possibilities, though quite limited in its initial incarnation. 

Basic Pattern Matching

We can now do things like 

Object o = "hello";
if (o instanceof String s) { System.out.println(s.toUpperCase());
}

Object o = "hello"; if (o instanceof String s) { System.out.println(s.toUpperCase()); }

Note the variable “s”, which is then used without any casting needed.

Deconstructing Optionals

It would be even nicer if we could deconstruct structured types at the same time. I think it’s coming in a future version, but I’m impatient. Let’s see how close we can get with the current tools.

One thing we can do is use a function on the left hand side of this expression.

Let’s consider the example of dealing with unknown values in Java. There’s a fair amount of legacy to deal with. Sometimes you’ll get a value, sometimes you’ll get a null. Sometimes you’ll get an Optional<T> , sometimes you’ll get an Optional<Optional<T>> etc.

How can we make unknowns nicer to deal with?

We could create a little utility function that lets us convert all of these forms of unknown into either a value or not. Then match with an instanceof test.

Object unknown = Optional.of("Hello World");
assertEquals( "hello world", unwrap(unknown) instanceof String s ? s.toLowerCase() : "absent"
);

Object unknown = Optional.of("Hello World"); assertEquals( "hello world", unwrap(unknown) instanceof String s ? s.toLowerCase() : "absent" );

Thanks to instanceof pattern matching we can just use the string directly, without having to resort to passing method references i.e optional.map(String::toLowercase) 

The unwrap utility itself uses pattern matching against Optional to recursively unwrap values from nested optionals. It also converts nulls and Optional.empty() to a non-instantiatable type for the purposes of ensuring they can never match the above pattern.

static Object unwrap(Object o) { if (o instanceof Optional<?> opt) { return opt.isPresent() ? unwrap(opt.get()) : None; } else if (o != null) { return o; } else { return None; }
}
static class None { private None() {} public static final None None = new None();
}

static Object unwrap(Object o) { if (o instanceof Optional<?> opt) { return opt.isPresent() ? unwrap(opt.get()) : None; } else if (o != null) { return o; } else { return None; } } static class None { private None() {} public static final None None = new None(); }

Here’s several more examples, if you’d like to explore further.

Deconstructing Records

What about more complex structures? Now that we have record types, wouldn’t it be great if we can deconstruct them to work with individual components more easily. I think until more powerful type patterns exist in the language we’ll have to diverge from the instanceof approach. 

I previously showed how we could do this for records we control, by having them implement an interface. What about records we do not control? How can we deconstruct those? 

This is about the closest I can get to what I’d hope would be possible as a first class citizen in the language in future.

record Name(String first, String last) {}
Object name = new Name("Benji", "Weber");
If.instance(name, (String first, String last) -> { System.out.println(first.toLowerCase() + last.toLowerCase()); // prints benjiweber
});

record Name(String first, String last) {} Object name = new Name("Benji", "Weber"); If.instance(name, (String first, String last) -> { System.out.println(first.toLowerCase() + last.toLowerCase()); // prints benjiweber });

It takes a record (Name) and a lambda where the method parameters are of the same types as the component types in the record. It deconstructs the record component parts and passes them to the lambda to use (assuming the record really matches).

We could also use as an expression to return a value, as long as we provide a fallback for the case when the pattern does not match.

Object zoo = new Zoo(new Duck("Quack"), new Dog("Woof"));
 
String result = withFallback("Fail"). If.instance(zoo, (Duck duck, Dog dog) -> duck.quack() + dog.woof() ); // result is QuackWoof

Object zoo = new Zoo(new Duck("Quack"), new Dog("Woof")); String result = withFallback("Fail"). If.instance(zoo, (Duck duck, Dog dog) -> duck.quack() + dog.woof() ); // result is QuackWoof

So how does this work? 

If.instance is a static method which takes an Object of unknown type (we hope it will be a Record), and a lambda function that we want to pattern match against the provided object.

How can we use a lambda as a type pattern? We can use the technique from my lambda type references article—have the lambda type be a SerializableLambda which will allow us to use reflection to read the types of each parameter. 

static <T,U,V> void instance(Object o, MethodAwareTriConsumer<T,U,V> action) {  
}

static <T,U,V> void instance(Object o, MethodAwareTriConsumer<T,U,V> action) { }

So we start with something like the above, a method taking an object and a reflectable lambda function.

Next we can make use of pattern matching again to check if it’s a record.

static <T,U,V> void instance(Object o, MethodAwareTriConsumer<T,U,V> action) { if (o instanceof Record r) { // now we know it's a record }
}

static <T,U,V> void instance(Object o, MethodAwareTriConsumer<T,U,V> action) { if (o instanceof Record r) { // now we know it's a record } }

Records allow reflection on their component parts. Let’s check whether we have enough component parts to match the pattern.

static <T,U,V> void instance(Object o, MethodAwareTriConsumer<T,U,V> action) { if (o instanceof Record r) { if (r.getClass().getRecordComponents().length < 3) { return; }
  // at this point we have a record with enough components and can use them. }
}

static <T,U,V> void instance(Object o, MethodAwareTriConsumer<T,U,V> action) { if (o instanceof Record r) { if (r.getClass().getRecordComponents().length < 3) { return; } // at this point we have a record with enough components and can use them. } }

Now we can invoke the passed action itself 

action.tryAccept((T) nthComponent(0, r), (U) nthComponent(1, r), (V) nthComponent(2, r));

action.tryAccept((T) nthComponent(0, r), (U) nthComponent(1, r), (V) nthComponent(2, r));

Where nthComponent uses reflection to access the relevant component property of the record.

private static Object nthComponent(int n, Record r) { try { return r.getClass().getRecordComponents()[n].getAccessor().invoke(r); } catch (Exception e) { throw new RuntimeException(e); }
}

private static Object nthComponent(int n, Record r) { try { return r.getClass().getRecordComponents()[n].getAccessor().invoke(r); } catch (Exception e) { throw new RuntimeException(e); } }

tryAccept is a helper default method I’ve added in MethodAwareTriConsumer. It checks whether the types of the provided values match the method signature before trying to pass them. Avoiding ClassCastException

interface MethodAwareTriConsumer<T,U,V> extends TriConsumer<T,U,V>, ParamTypeAware { default void tryAccept(T one, U two, V three) { if (acceptsTypes(one, two, three)) { accept(one, two, three); } } default boolean acceptsTypes(Object one, Object two, Object three) { return paramType(0).isAssignableFrom(one.getClass()) && paramType(1).isAssignableFrom(two.getClass()) && paramType(2).isAssignableFrom(three.getClass()); }
  default Class<?> paramType(int n) { int actualParameters = method().getParameters().length; // captured final variables may be prepended int expectedParameters = 3; return method().getParameters()[(actualParameters - expectedParameters) + n].getType(); }
}

interface MethodAwareTriConsumer<T,U,V> extends TriConsumer<T,U,V>, ParamTypeAware { default void tryAccept(T one, U two, V three) { if (acceptsTypes(one, two, three)) { accept(one, two, three); } } default boolean acceptsTypes(Object one, Object two, Object three) { return paramType(0).isAssignableFrom(one.getClass()) && paramType(1).isAssignableFrom(two.getClass()) && paramType(2).isAssignableFrom(three.getClass()); } default Class<?> paramType(int n) { int actualParameters = method().getParameters().length; // captured final variables may be prepended int expectedParameters = 3; return method().getParameters()[(actualParameters - expectedParameters) + n].getType(); } }

Then put all this together and we can pattern match against Objects of unknown type, and deconstruct them if they’re records matching the provided lambda type-pattern.

record Colour(Integer r, Integer g, Integer b) {}
 
Object unknown = new Colour(5,6,7); // note the Object type
 
int result = withFallback(-1). If.instance(unknown, (Integer r, Integer g, Integer b) -> r + g + b );
 
assertEquals(18, result);

record Colour(Integer r, Integer g, Integer b) {} Object unknown = new Colour(5,6,7); // note the Object type int result = withFallback(-1). If.instance(unknown, (Integer r, Integer g, Integer b) -> r + g + b ); assertEquals(18, result);

Degrading safely if the pattern does not match

Object unknown = new Name("benji", "weber");
 
int result = withFallback(-1). If.instance(unknown, (Integer r, Integer g, Integer b) -> r + g + b );
 
assertEquals(-1, result);

Object unknown = new Name("benji", "weber"); int result = withFallback(-1). If.instance(unknown, (Integer r, Integer g, Integer b) -> r + g + b ); assertEquals(-1, result);

Code for the record deconstruction and several more examples all in this test on github. Hopefully all this will be made redundant by future enhancements to Java’s type patterns :)