Abstract: Sealed classes show us which subclasses they permitted. Unfortunately there is no way to do this recursively. In this newsletter we show how to navigate the sealed class hierarchy and write a ClassSpliterator that can then create a Stream of classes.
Welcome to the 298th edition of The Java(tm) Specialists' Newsletter, sent to you from what is often called the "largest aircraft carrier in the Mediterranean", the Island of Crete. Not too far from our house is a NATO base, a US Navy and Airforce base, and a Greek base. On clear days, we often have F16s flying overhead as we try hold a conversation running on Kalathas Beach. I joke that the F16s are always flying in groups of either 2 or three. When a tourist says to me - "no, those were 4 this time", I would then retort with: "Ah no, those were two groups of 2." No matter the number, I always have a smart-ass answer using my magic "2 or 3".
As you can probably imagine, I am a bit distracted at the moment, and thus sitting down and writing has been particularly tedious. Whenever I get started, Twitter beckons me, and another hour passes. You can probably relate to that! [And if you're reading this in the future, look up what was going on at the end of February 2022.]
javaspecialists.teachable.com: Please visit our new self-study course catalog to see how you can upskill your Java knowledge.
In Java, classes are loaded as and when they are referenced. It is thus quite difficult to find all subclasses of one particular class. This is rather annoying if we want to write some test code that verifies that all implementations of an interface have been implemented correctly.
Since Java 17, we have sealed classes. These allow us to
list precisely which subclasses will be permitted. Each of
the subclasses has to be marked as final
,
sealed
or non-sealed
.
The final
classes cannot be
subclassed further. The sealed
classes again have to list precisely who can extend them.
Lastly, the non-sealed
classes can
be extended in any way we wish.
Since all the permitted subclasses have to be explicitly specified, we can also find out from a class what those are. Let's take the example of the ConstantDesc class:
import java.lang.constant.*; import java.util.*; public class PermittedSubclassesOfConstantDesc { public static void main(String... args) { Arrays.stream(ConstantDesc.class.getPermittedSubclasses()) .forEach(System.out::println); } }
We see that there are these permitted subclasses, and no others:
interface java.lang.constant.ClassDesc interface java.lang.constant.MethodHandleDesc interface java.lang.constant.MethodTypeDesc class java.lang.Double class java.lang.constant.DynamicConstantDesc class java.lang.Float class java.lang.Integer class java.lang.Long class java.lang.String
The primitive wrappers and String are final
,
but the others are not. This means that, for example,
ClassDesc
also needs to be either
sealed
or
non-sealed
. Interfaces and
abstract classes cannot be final
.
Here is a SealedClassPrinter that recursively prints the tree of permitted subclasses from a root sealed class:
import java.lang.constant.*; import java.lang.reflect.*; public class SealedClassPrinter { private static final int TAB = 4; public static void main(String... args) { printTree(ConstantDesc.class, 0); } public static void printTree(Class<?> clazz) { printTree(clazz, 0); } private static void printTree(Class<?> clazz, int level) { String indent = " ".repeat(level * TAB); String modifier = clazz.isSealed() ? "sealed" : Modifier.isFinal(clazz.getModifiers()) ? "final" : "non-sealed"; String name = clazz.getSimpleName(); System.out.printf("%s%s %s%n", indent, modifier, name); var permittedSubclasses = clazz.getPermittedSubclasses(); if (permittedSubclasses != null) { for (var subclass : permittedSubclasses) { printTree(subclass, level + 1); } } } }
Here is the output from the code:
sealed ConstantDesc sealed ClassDesc final PrimitiveClassDescImpl final ReferenceClassDescImpl sealed MethodHandleDesc final AsTypeMethodHandleDesc sealed DirectMethodHandleDesc final DirectMethodHandleDescImpl sealed MethodTypeDesc final MethodTypeDescImpl final Double non-sealed DynamicConstantDesc final Float final Integer final Long final String
Since the DynamicConstantDesc
subclass is
non-sealed
, we cannot determine what
other subclasses there might be.
We could also create a Spliterator of the permitted subclasses, which we can then use to produce a Stream.
import java.util.*; import java.util.function.*; public class ClassSpliterator implements Spliterator<Class<?>> { private Class<?> nextClass; private final Deque<Iterator<Class<?>>> unfinished = new ArrayDeque<>(); public ClassSpliterator(Class<?> root) { if (!root.isSealed()) throw new IllegalArgumentException(root + " not sealed"); nextClass = root; addPermittedSubclasses(root); } private void addPermittedSubclasses(Class<?> root) { Optional.ofNullable(root.getPermittedSubclasses()) .map(Arrays::asList) .map(Iterable::iterator) .ifPresent(unfinished::addLast); } @Override public boolean tryAdvance(Consumer<? super Class<?>> action) { Objects.requireNonNull(action); if (nextClass == null) nextClass = findNextClass(); if (nextClass == null) return false; action.accept(nextClass); nextClass = null; return true; } private Class<?> findNextClass() { while (!unfinished.isEmpty()) { Iterator<Class<?>> iterator = unfinished.peekLast(); if (iterator.hasNext()) { var result = iterator.next(); addPermittedSubclasses(result); return result; } else { unfinished.removeLast(); } } return null; } @Override public Spliterator<Class<?>> trySplit() { // never support splitting return null; } @Override public long estimateSize() { // unknown size return Long.MAX_VALUE; } @Override public int characteristics() { return IMMUTABLE | NONNULL; } }
Here is an example how we create a stream from the ClassSpliterator and then we print out the classes:
import java.lang.constant.*; import java.util.stream.*; public class StreamOfConstantDesc { public static void main(String... args) { Stream<Class<?>> stream = StreamSupport.stream( new ClassSpliterator(ConstantDesc.class), false ); stream.forEach(System.out::println); } }
Output is as follows:
interface java.lang.constant.ConstantDesc interface java.lang.constant.ClassDesc class java.lang.constant.PrimitiveClassDescImpl class java.lang.constant.ReferenceClassDescImpl interface java.lang.constant.MethodHandleDesc class java.lang.constant.AsTypeMethodHandleDesc interface java.lang.constant.DirectMethodHandleDesc class java.lang.constant.DirectMethodHandleDescImpl interface java.lang.constant.MethodTypeDesc class java.lang.constant.MethodTypeDescImpl class java.lang.Double class java.lang.constant.DynamicConstantDesc class java.lang.Float class java.lang.Integer class java.lang.Long class java.lang.String
I debated with myself whether the spliterator should be DISTINCT. Alas, that would not be correct, because we can have funky trees where a class appears multiple times. Here is an example. Note that we do not need to explicitly specify which subclasses are permitted, since all the classes are defined in the same scope.
public class FunkyTreeDemo { public static void main(String... args) { SealedClassPrinter.printTree(A.class); } sealed interface A {} sealed interface B extends A {} sealed interface C extends B {} static final class D implements A, B, C {} }
We see the output as the follows, note that D occurs three times:
sealed A sealed B sealed C final D final D final D
Once we have a stream, we can easily make that distinct by
adding a call to distinct()
.
Anyway, that is all I have for you this month. Keep safe and I hope to be able to write to you again in March!
Heinz
We are always happy to receive comments from our readers. Feel free to send me a comment via email or discuss the newsletter in our JavaSpecialists Slack Channel (Get an invite here)
We deliver relevant courses, by top Java developers to produce more resourceful and efficient programmers within their organisations.