Java 17 Features: Pattern Matching for switch and Sealed Classes

Oskar
7 min readSep 25, 2021

--

Photo by NordWood Themes on Unsplash

The new LTS (Long Term Support) release rev 17 is globally available and ready for production use. Though many of us will not update immediately ( or any time in the near future ) for existing projects this is a great release that will likely be the default version for bootstrapping new projects throughout following few years as soon as libraries and frameworks catch up. Taking Spring for example Spring Framework rev 6 and Spring Boot rev 3 planned to release late next year (2022.) will baseline Java 17+ support.

I had an interesting experience a couple of months ago staring a new microservice using latest Spring Boot with Java 16 and there were a few workarounds I had to make to get my service up and running that consisted of couple JMS listeners and few REST endpoints. Main hustle for me was deserializing JMS messages to Java Records via Jackson, I had so many issues that I had to transform my records to good old POJOs (using Lombok to make code as clean as possible compared to records) to get things working. Though later I found that that was a known headache for many people trying out Java 16 and that supposedly with latest Jackson version issue is completely fixed. Since most of the industry doesn’t really care about non LTS releases having issues like that is completely expected, but with 17 we should se much better support and prompt response from most major libraries and framework, just as it happened when rev 11 was released a couple years ago.

Considering that most will go from 8 or 11 straight to 17 we will be getting dozens of new features and internal improvements released in the last six major versions, or even last nine versions if you are upgrading from Java 8. Covering all of those changes would result in a mega blog or even a full book, so I will only cover major features released between 16 and 17. If you haven’t caught up with prior six Java versions I have many of them covered in the previous blogs I released in the last few years.

Java 17 really has only two major features releasing, the first one is a long awaited preview of Pattern Matching for switch and the second one is Sealed Classes getting out of two preview cycles ( 15 and 16 ) and coming out as full fledged feature ready for production usages. You can read full release notes with all other minor and internal JDK changes on the openjdk 17 page.

Pattern Matching for switch (preview)

Way back in Java 12 switch was enhanced so that it can be used as either a statement or an expression. After two preview cycles in Java 12 and 13 feature was fully released in Java 14. Continuing on that work switch is further enhanced to support pattern matching workflows meaning that we can use patterns now in case labels.

Switch was limited in a way that you could only switch on values of a few types: numeric types, Enum types, and strings and you can only test for exact equality against constants. So, for example, if you needed multiple instanceof tests you ended up using long, hardly readable if-else chains, and switch just seems to be more natural way for those kind of tests.

With this feature switch statements and expressions are extended to work on any type, and allow case labels with patterns, rather than just constants, resulting in replacing long if-else chains in instanceof workflows with something like this:

static String formatterPatternSwitch(Object o) {
return switch (o) {
case Integer i -> String.format("int %d", i);
case Long l -> String.format("long %d", l);
case Double d -> String.format("double %f", d);
case String s -> String.format("String %s", s);
default -> o.toString();
};
}

Code example comes directly from JEP documentation. This gives better, more readable and more natural way of type checking compared to if-else chains.

All older switch workflows work as they did, as always backwards compatibility was not jeopardized with the new features. One of the cases that needed special handling is working with null values. Before if you passed null as a switch selector you would immediately get Null Pointer Exception, but since now switch selectors can be any type (not only numeric, Enum or String) working with null might be a valid use case.

Now, the behavior with null selector is determined by the case labels, if you have an explicit null case label then it’s valid to pass a null to a switch selector. But if you don’t have a case label with null value then passing null as a selector will result in the NPE to preserve backward compatibility.

Sometimes other than only matching a type you might need some additional refinements. Consider this code from the docs:

static void testTriangle(Shape s) {
switch (s) {
case null ->
System.out.println("Passed shape is null");
case Triangle t && (t.calculateArea() > 100) ->
System.out.println("Large triangle");
case Triangle t ->
System.out.println("Small triangle");
default ->
System.out.println("Non-triangle");
}
}

It’s easy to read and self explanatory, the second case label refines not only by the type but also by the calculateArea method.

In the example above I also included the null case that I mentioned, passing a null for a selector ( variable s) in this setup will be fine since we have a explicit null case label. If we didn’t have a case for null value, passing s as a null would result in immediate termination via NPE.

Sealed Classes

Sealed classed went through two previews, first one in Java 15, and after a few refinements, a second one in Java 16 and now they are ready to be fully released as a new standard language feature. This is a pretty important feature not only because of the currently obvious usages but also since it was designed in a way that directly supports future pattern matching workflows.

A common process we have when creating new classes and interfaces is deciding which scope modifier should we use, it’s always case by case and until now the options language was providing weren’t granular enough.

This issue is way more prevalent when you have a project where using default (package-private) scope modifier is not encouraged by the official style guide and making a class (that needs to be inherited at some point) either public or protected is sometimes just too generous.

Now we have fine grained inheritance control using sealed scope modifier for classes and interfaces. So if you need your super class or an interface to be widely accessible but not arbitrary extensible, sealed modifier is your friend.

Example from the JDK documentation:

package com.example.geometry;public sealed class Shape
permits Circle, Rectangle, Square {...}

If you have nested classes or more than one class in the same source file you can omit permits for those classes, Java compiler will infer permitted subclasses as long as they are in the same file.

That means in the example above, as long as Shape, Circle, Rectangle and Square are in the same source file you can omit permits and your code can look something like this:

package com.example.geometry;sealed class Shape {...}
... class Circle extends Shape {...}
... class Rectangle extends Shape {...}
... class Square extends Shape {...}

This might be a rare case, but if you want to save a few bytes by not writing permits in an example above you can do it, compiler will know what to do.

Permitted classes can be final , sealed or non-sealed.

package com.example.geometry;public sealed class Shape
permits Circle, Rectangle, Square {...}
public final class Circle extends Shape {...}public sealed class Rectangle extends Shape
permits TransparentRectangle, FilledRectangle {...}
public final class TransparentRectangle extends Rectangle {...}
public final class FilledRectangle extends Rectangle {...}
public non-sealed class Square extends Shape {...}

Final will prevent its part of the class hierarchy from being extended further.

Sealed will allow its part of the hierarchy to be extended further than super class originally defined, but only by classes that sealed subclass permits.

And finally non-sealed will return things back into the wild and revert the class hierarchy to being open for extension by unknown sub classes, of course starting from the child non-sealed class, super class is still sealed so it’s still off limits for unknown extension.

Sealed classes work really well with records, caveat is that records are implicitly final , so by design your sub class (sub record) modifier options are reduced from final , sealed or non-sealed to, well, only final

As mentioned in the beginning, this feature is designed to work hand in hand with pattern matching workflows.

JDK docs example with classes used in earlier code snippets of using if-else chain for instanceof tests:

Shape rotate(Shape shape, double angle) {
if (shape instanceof Circle) return shape;
else if (shape instanceof Rectangle) return shape;
else if (shape instanceof Square) return shape;
else throw new IncompatibleClassChangeError();
}

Java has no way of knowing if you covered every possible instance of Shape, if you are missing one or more of them Java will never know and doesn’t have a way to let you know that you missed a sub type.

That is where Sealed Classes can come into play, if you are working with a sealed hierarchy compiler can know exactly which sub types you are missing in the instance checks and issue you a compile time error if you missed a case.

Shape rotate(Shape shape, double angle) {
return switch (shape) { // pattern matching switch
case Circle c -> c;
case Rectangle r -> shape.rotate(angle);
case Square s -> shape.rotate(angle);
// no default needed!
}
}

In the example above using both new Java 17 features, pattern matching for switch and sealed classes, compiler will actually issue you an error message if you are missing a switch case for any class that is permitted to extend / implement Shape.

Java’s Reflection API was also extended to add support for sealed classes, there are two new methods, both pretty self explanatory:

Class<?>[] getPermittedSubclasses();
boolean isSealed();

That’s it for the new features in the latest Java release. Thanks for reading to the end and happy coding.

--

--