Java 17's sealed classes and the Immutables project

Posted May 13, 2021 by Bart Naudts ‐ 2 min read

How e2immu benefits from sealed classes, introduced by Java 17, to annotate classes generated by Immutables

The concept of sealed classes, as described in JEPS-409, allows you to fully control sub-classing, by enumerating the possible sub-types explicitly. It goes beyond the final modifier for classes, which simply_prohibits_ sub-classing.

My first reaction to sealed classes was so what?. Probably nothing I’d ever make use of. However, studying how to integrate the builders and immutable types generated by the Immutables project, I came across an obvious use case of sealed classes!

In the Immutables way of working, you specify an immutable class and its builder using an interface (example taken from their website):

@Value.Immutable
public interface ValueObject extends WithValueObject {
  String getName();
  List<Integer> getCounts();
  Optional<String> getDescription();

  class Builder extends ImmutableValueObject.Builder {}
  // ImmutableValueObject.Builder will be generated and
  // our builder will inherit and reexport methods as it's own.
  // Static nested Builder will inherit all the public method
  // signatures of ImmutableValueObject.Builder
} 

The Immutables annotation processor generates a class, ImmutableValueObject, which looks like

@Generated(from = "ValueObject", generator = "Immutables")
@SuppressWarnings({"all"})
@javax.annotation.processing.Generated("org.immutables.processor.ProxyProcessor")
public final class ImmutableValueObject implements ValueObject {
    ...
}

(I’m deliberately skipping all immutability aspects of the Immutables-generated classes in this post; there’ll be another post shortly to show how Immutables integrates with e2immu. In this particular example, ValueObject becomes @E2Container; as does List<Integer> getCounts().)

Suppose we interpret one of the @Generated annotations as “this class is the only implementation of ValueObject”. This is equivalent to sealing ValueObject, with a ImmutableValueObject as its only allowed implementation. e2immu will benefit as follows: annotations computed on the implementation ImmutableValueObject, its fields, and its methods, can travel upwards to the interface! No need to manually annotate the interface.

Summarizing: when an abstract type is sealed, the annotations that e2immu computes on the implementation can be transferred to the interface, exactly because no other, as yet unknown implementations are possible.