Running on Java 24-ea+24-2960 (Preview)
Home of The JavaSpecialists' Newsletter

298Finding Permitted Subclasses

Author: Др. Ґайнц М. КабуцDate: 2022-02-28Java Version: 17Category: Language
 

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.

Finding Permitted Subclasses

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

 

Comments

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)

When you load these comments, you'll be connected to Disqus. Privacy Statement.

Related Articles

Browse the Newsletter Archive

About the Author

Heinz Kabutz Java Conference Speaker

Java Champion, author of the Javaspecialists Newsletter, conference speaking regular... About Heinz

Superpack '23

Superpack '23 Our entire Java Specialists Training in one huge bundle more...

Free Java Book

Dynamic Proxies in Java Book
Java Training

We deliver relevant courses, by top Java developers to produce more resourceful and efficient programmers within their organisations.

Java Consulting

We can help make your Java application run faster and trouble-shoot concurrency and performance bugs...