Sealed types

Brian Goetz brian.goetz at oracle.com
Tue Nov 27 22:20:54 UTC 2018


Since we’re already discussing one of the consequences of sealed types, 
let’s put the whole story on the table. These are my current thoughts, 
but there’s room in the design space to wander a bit.

*Definition.* A /sealed type/ is one for which subclassing is restricted 
according to guidance specified with the type’s declaration; finality 
can be considered a degenerate form of sealing, where no subclasses at 
all are permitted. Sealed types are a sensible means of modeling 
/algebraic sum types/ in a nominal type hierarchy; they go nicely with 
records (/algebraic product types/), though are also useful on their own.

Sealing serves two distinct purposes. The first, and more obvious, is 
that it restricts who can be a subtype. This is largely a 
declaration-site concern, where an API owner wants to defend the 
integrity of their API. The other is that it potentially enables 
exhaustiveness analysis at the use site when switching over sealed types 
(and possibly other features.) This is less obvious, and the benefit is 
contingent on some other things, but is valuable as it enables better 
compile-time type checking.

/Note:/ It is probably desirable to ship this with records, though it 
could be shipped before or after.

*Declaration.* We specify that a class is sealed by applying the 
|sealed| modifier to a class, abstract class, interface, or record:

|sealed interface Node { ... } |

In this streamlined form, |Node| may be extended only by its nestmates. 
This may be suitable for many situations, but not for all; in this case, 
the user may specify an explicit |permits| list:

|sealed interface Node permits FooNode, BarNode { ... } |

The two forms may not be combined; if there is a permits list, it must 
list all the permitted subtypes. We can think of the simple form as 
merely inferring the |permits| clause from information in the same 
compilation unit.

*Exhaustiveness.* One of the benefits of sealing is that the compiler 
can enumerate the permitted subtypes of a sealed type; this in turn lets 
us perform exhaustiveness analysis when switching over patterns 
involving sealed types. (In the simplified form, the compiler computes 
the permits list by enumerating the subtypes in the nest when the nest 
is declared, since they are in a single compilation unit.) Permitted 
subtypes must belong to the same module (or, if not in a module, the 
same package and protection domain.)

/Note:/ It is superficially tempting to have a relaxed but less explicit 
form, say which allows for a type to be extended by package-mates or 
module-mates without listing them all. However, this would undermine the 
compiler’s ability to reason about exhaustiveness. This would achieve 
the desired subclassing restrictions, but not the desired ability to 
reason about exhaustiveness.

*Classfile.* In the classfile, a sealed type is identified with an 
|ACC_SEALED| modifier (we could choose to overload |ACC_FINAL| for bit 
preservation), and a |Sealed| attribute which contains a list of 
permitted subtypes (similar in structure to the nestmate attributes.)

*Transitivity.* Sealing is transitive; unless otherwise specified, an 
abstract subtype of a sealed type is implicitly sealed, and a concrete 
subtype of a sealed type is implicitly final. This can be reversed by 
explicitly modifying the subtype with the |non-sealed| or |non-final| 
modifiers.

Unsealing a subtype in a hierarchy doesn’t undermine the sealing, 
because the (possibly inferred) set of explicitly permitted subtypes 
still constitutes a total covering. However, users who know about 
unsealed subtypes can use this information to their benefit (much like 
we do with exceptions today; you can catch FileNotFoundException 
separately from IOException if you want, but don’t have to.)

/Note:/ Scala made the opposite choice with respect to transitivity, 
requiring sealing to be opted into at all levels. This is widely 
believed to be a source of bugs; it is rare that one actually wants a 
subtype of a sealed type to not be sealed. I suspect the reasoning in 
Scala was, at least partially, the desire to not make up a new keyword 
for “not sealed”. This is understandable, but I’d rather not add to the 
list of “things for which Java got the defaults wrong.”

An example of where explicit unsealing (and private subtypes) is useful 
can be found in the JEP-334 API:

|sealed interface ConstantDesc permits String, Integer, Float, Long, 
Double, ClassDesc, MethodTypeDesc, MethodHandleDesc, DynamicConstantDesc 
{ } sealed interface ClassDesc extends ConstantDesc permits 
PrimitiveClassDescImpl, ReferenceClassDescImpl { } private class 
PrimitiveClassDescImpl implements ClassDesc { } private class 
ReferenceClassDescImpl implements ClassDesc { } sealed interface 
MethodTypeDesc extends ConstantDesc permits MethodTypeDescImpl { } 
sealed interface MethodHandleDesc extends ConstantDesc permits 
DirectMethodHandleDesc, MethodHandleDescImpl { } sealed interface 
DirectMethodHandleDesc extends MethodHandleDesc permits 
DirectMethodHandleDescImpl // designed for subclassing non-sealed class 
DynamicConstantDesc extends ConstantDesc { ... } |

*Enforcement.* Both the compiler and JVM should enforce sealing.

/Note:/ It might be allowable for VM support to follow in a later 
version, rather than delaying the feature entirely.

*Accessibility.* Subtypes need not be as accessible as the sealed 
parent. In this case, clients are not going to get the chance to 
exhaustively switch over them; they’ll have to make these switches 
exhaustive with a |default| clause or other total pattern. When 
compiling a switch over such a sealed type, the compiler can provide a 
useful error message (“I know this is a sealed type, but I can’t provide 
full exhaustiveness checking here because you can’t see all the 
subtypes, so you still need a default.”)

*Javadoc.* The list of permitted subtypes should probably be considered 
part of the spec, and incorporated into the Javadoc. Note that this is 
not exactly the same as the current “All implementing classes” list that 
Javadoc currently includes, so a list like “All permitted subtypes” 
might be added (possibly with some indication if the subtype is less 
accessible than the parent.)

/Open question:/ With the advent of records, which allow us to define 
classes in a single line, the “one class per file” rule starts to seem 
both a little silly, and constrain the user’s ability to put related 
definitions together (which may be more readable) while exporting a flat 
namespace in the public API. I think it is worth considering relaxing 
this rule to permit for sealed classes, say: allowing public auxilliary 
subtypes of the primary type, if the primary type is public and sealed.
​
/Syntactic alternative:/ Rather than inventing a new modifier (which 
then needs a negation modifier), we could generalize the meaning of 
|final|, such as:

|final class C { } // truly final final interface Node // explicit 
version permits ANode, BNode { } non-final class ANode implements Node { 
} // opt-out of transitivity final interface Node // inferred version 
permits <bikeshed> { } |

This eliminates both |sealed| and |non-sealed| as modifiers. The 
downside is, of course, that it will engender some “who moved my cheese” 
reactions. (We currently have separate spellings for |extends| and 
|implements|; some think this was a mistake, and it surely complicated 
things when we got to generics, as we created an asymmetry with |<T 
extends U>|. The move of retconning |final| avoids replicating this 
mistake.) This is in line with the suggested retcon at the classfile 
level as well.

​
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.openjdk.java.net/pipermail/amber-spec-experts/attachments/20181127/07b96468/attachment-0001.html>


More information about the amber-spec-experts mailing list