There are two language features:
Sealed classes
Pattern matching for switch (Preview)
Sealed classes
- Allow the author of a class or interface to control which code is responsible for implementing it.
- Provide a more declarative way than access modifiers to restrict the use of a superclass.
- Support future directions in pattern matching by underpinning the exhaustive analysis of patterns.
Here a simple example:
package com.example.geometry; public abstract sealed class Shape permits Circle, Rectangle, Square {...} //TODO: define Circle //TODO: define Rectamgle //TODO: define Square
A class is sealed by applying the sealed modifier to its declaration.
Then the permits clause specify the classes that can extend it.
Every permitted sub class must directly extend the sealed class.
And every permitted sub class must choose a modifier to describe how it continue the sealing initiated by the super class.
It could be final and allow no further sub classing:
package com.example.geometry; public abstract sealed class Shape permits Circle, Rectangle, Square {...} public final class Circle extends Shape {...} //TODO: define Rectamgle //TODO: define Square
It could in turn be sealed which means it will have it own set of permitted classes:
package com.example.geometry; public abstract sealed class Shape permits Circle, Rectangle, Square {...} public sealed class Rectangle extends Shape permits TransparentRectangle, FilledRectangle {...} public final class TransparentRectangle extends Rectangle {...} public final class FilledRectangle extends Rectangle {...} //TODO: define Circle //TODO: define Square
Or it can be non-sealed.
A sealed class can not prevent permitted classes from doing this.
package com.example.geometry; public abstract sealed class Shape permits Circle, Rectangle, Square {...} public non-sealed class Square extends Shape {...} //TODO: define Circle //TODO: define Rectangle ;
One and only one of this modifier must be used by each permitted sub class.
Java lang class will get two new methods that call using reflection will be able to determine if a class or interface is sealed.
And if so get a list of permitted classes.
Pattern Matching for switch Preview
Enhance the Java programming language with pattern matching for switch expressions and statements.
Pattern matching on switch allows an expression to be tested against a number of patterns, each with a specific action, so that complex data-oriented queries can be expressed concisely and safely
- Allow patterns to appear in case labels
- Introduces case null statement
Current switch statement works on a few types: numeric, enum and strings.
If we want to use patterns to check for a certain number of possibilities, we need to use a chain of 'if else' statements.
static String formatter(Object o) { String formatted = "unknown"; if (o instanceof Integer i) { formatted = String.format("int %d", i); } else if (o instanceof Long 1) { formatted = String.format("long %d", 1); } else if (o instanceof Double d) { formatted = String.format("double %f", d); } else if (o instanceof String s) { formatted = String.format("String %s", s); } return formatted; }
Using pattern matching in a switch, we can make this clearer.
Remember that the case statements has to be exhaustive so in most cases, you will have to add a default value.
static String formatterPatternSwitch(Object o) { return switch (o) { case Integer i - > String.format("int %d", i); case Long 1 - > String.format("long %d", 1); case Double d - > String.format("double %f", d); case String s - > String.format("String %s", s); default - > o.toString(); }; }
But there is one more thing missing here.
Traditionnally, switch statements and switch expressions throw a NullPointerException if the selected expression evaluates to null.
Testing for null, it outside the switch statement:
static String formatterPatternSwitch(Object o) { if (o==null) { return "oof"; } return switch (o) { case Integer i - > String.format("int %d", i); case Long 1 - > String.format("long %d", 1); case Double d - > String.format("double %f", d); case String s - > String.format("String %s", s); default - > o.toString(); }; }
But now that case label will allow type pattern, we can add a null case to the switch statement:
static String formatterPatternSwitch(Object o) { return switch (o) { case null - > "null"; case Integer i - > String.format("int %d", i); case Long 1 - > String.format("long %d", 1); case Double d - > String.format("double %f", d); case String s - > String.format("String %s", s); default - > o.toString(); }; }
This makes the code easier to read and maintain.
After a successful pattern match, we often need further tests which can lead to cumbersome code like this:
static void test(Object o) { switch (o) { case String s: if (s.length() == 1) { ... } else { ... } break; ... } }
The desired test: if o is a String of length 1 is split between case and if statement.
We can improve readability if the pattern supports combination of pattern and boolean expression:
static void test(Object a) { switch (o) { case String s && (s.length() == 1) -> ... case String s -> ... ... }; }
First case matches for string of length 1. Second case matches strings of other lengths.
Pattern matching for switch
Possible issues
There are a couple of new gotchas with the rise of pattern matching for switch.
Dominance of pattern labels
The first one is that a pattern could dominate later patterns.
It is a compile-time error if a pattern label in a switch block is dominated by an earlier pattern :
It is possible to have an expression match multiple labels in a switch block.
This is allowed but it must be placed in the right order.
The first example here is very similar to the previous one but with the order reversed:
switch (o) { case String s - > ... case String s && (s.length() == 1) - > // Error- pattern is dominated by previous pattern }
Since the first label will catch all strings including strings of length one, the second label is never reached.
It is dominated by an earlier case.
This will result in a compile time error.
No fall through allowed when declaring a pattern variable
An other possible error, it to have a fall through when declaring a pattern variable:
switch (o) { case Character c: System.out.println("character"); case Integer i: //Error. Can't fall through! System.out.println("An integer " + i); ... }
In this example, if the variable o was of type Character, the code would print 'character'.
But then, as there is no reasonable break statements, execution will flow to the next case label.
Since the variable i declared in the second pattern would not have been initialized, we get an error.
It is therefore a compile-time error to allow flow through to a case that declares a pattern.
For infer a label that doesn't declare a pattern, it is fine.