Hooking sealed classes in switch – Sealed and Hidden Classes
172. Hooking sealed classes in switch
It is not the first time in this book that we present an example of Sealed Classes and switch expressions. In Chapter 2, Problem 61, we have briefly introduced such an example via the sealed Player interface with the goal of covering completeness (type coverage) in pattern labels for switch.If, at that time, you found this example confusing I’m pretty sure that now is clear. However, let’s keep things fresh and let’s have another example starting from this abstract base class:
public abstract class TextConverter {}
And, we have three converters available as follows:
final public class Utf8 extends TextConverter {}
final public class Utf16 extends TextConverter {}
final public class Utf32 extends TextConverter {}
Now, we can write a switch expression to match these TextConverter as follows:
public static String convert(
TextConverter converter, String text) {
return switch (converter) {
case Utf8 c8 -> “Converting text to UTF-8: ” + c8;
case Utf16 c16 -> “Converting text to UTF-16: ” + c16;
case Utf32 c32 -> “Converting text to UTF-32: ” + c32;
case TextConverter tc -> “Converting text: ” + tc;
default -> “Unrecognized converter type”;
};
}
Check out the highlighted lines of code. After the three cases (case Utf8, case Utf16, and case Utf32) we must have one of the case TextConverter or the default case. In other words, after matching Utf8, Utf16, and Utf32, we must have a total type pattern (unconditional pattern) to match any other TextConverter or a default case which typically means that we are facing an unknown converter.If both, the total type pattern and the default label are missing then the code doesn’t compile. The switch expression doesn’t cover all the possible cases (input values) therefore is not exhaustive. This is not allowed, since switch expressions and switch statements that use null and/or pattern labels should be exhaustive.The compiler will consider our switch as non-exhaustive because we can freely extend the base class (TextConverter) with uncovered cases. An elegant solution is to seal the base class (TextConverter) as follows:
public sealed abstract class TextConverter
permits Utf8, Utf16, Utf32 {}
And, now the switch can be expressed as follows:
return switch (converter) {
case Utf8 c8 -> “Converting text to UTF-8: ” + c8;
case Utf16 c16 -> “Converting text to UTF-16: ” + c16;
case Utf32 c32 -> “Converting text to UTF-32: ” + c32;
};
This time, the compiler knows all the possible TextConverter types and sees that are all covered in the switch. Since TextConverter is sealed there are no surprises, no uncovered cases can occur. Nevertheless, if later we decide to add a new TextConverter (for instance, we add Utf7 by extending TextConverter and adding this extension in the permits clause) then the compiler will immediately complain that the switch is non-exhaustive, so we must take action and add the proper case for it.At this moment, Utf8, Utf16, and Utf32 are declared as final, so they cannot be extended. Let’s assume that Utf16 is modified to become non-sealed:
non-sealed public class Utf16 extends TextConverter {}
Now, we can extend Utf16 as follows:
public final class Utf16be extends Utf16 {}
public final class Utf16le extends Utf16 {}
Even if we added two subclasses to Utf16 class, our switch is still exhaustive because the case Utf16 will cover Utf16be and Utf16le as well. Nevertheless, we can explicitly add cases for them as long as we add these cases before case Utf16 as follows:
return switch (converter) {
case Utf8 c8 -> “Converting text to UTF-8: ” + c8;
case Utf16be c16 -> “Converting text to UTF-16BE: ” + c16;
case Utf16le c16 -> “Converting text to UTF-16LE: ” + c16;
case Utf16 c16 -> “Converting text to UTF-16: ” + c16;
case Utf32 c32 -> “Converting text to UTF-32: ” + c32;
};
We have to add case Utf16be and case Utf16le before case Utf16 to avoid dominance errors (Chapter 2, Problem 60).Here is another example of combining Sealed Classes, Pattern Matching for Switch and Java Records for computing the sum of nodes in a binary tree of integers:
sealed interface BinaryTree {
record Leaf() implements BinaryTree {}
record Node(int value, BinaryTree left, BinaryTree right)
implements BinaryTree {}
}
static int sumNode(BinaryTree t) {
return switch (t) {
case Leaf nl -> 0;
case Node nv -> nv.value() + sumNode(nv.left())
+ sumNode(nv.right());
};
}
And, here is an example of calling sumNode():
BinaryTree leaf = new Leaf();
BinaryTree s1 = new Node(5, leaf, leaf);
BinaryTree s2 = new Node(10, leaf, leaf);
BinaryTree s = new Node(4, s1, s2);
int sum = sumNode(s);
In this example, the result is 19.