Sealed types

Brian Goetz brian.goetz at oracle.com
Fri Dec 7 16:38:53 UTC 2018


I’ve updated the document on sealing to reflect the discussion so far.


        Sealed Classes

*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.

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

|final interface Node permits A, B, C { ... } |

In this explicit form, |Node| may be extended only by the types 
enumerated in the |permits| list (which must further be members of the 
same package or module.)

In many situations, this may be overly explicit; if all the subtypes are 
declared in the same compilation unit, we may wish to permit a 
streamlined form of the |permits| clause, that means “may be extended by 
classes in the same compilation unit.”

|final interface Node permits __nestmates { ... } |

(As usual, pseudo-keywords beginning with |__| are placeholders to 
illustrate the overall shape of the syntax.)

We can think of the simpler form as merely inferring the full |permits| 
clause from information already present in the source file.

Anonymous subclasses (and lambdas) of a sealed type are prohibited.

*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.)

/Note:/ It is superficially tempting to say |permits package| or 
|permits module| as a shorthand, which would allow 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, because packages and modules are not always co-compiled. 
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_FINAL| accessibility bit, and a |PermittedSubtypes| attribute which 
contains a list of permitted subtypes (similar in structure to the 
nestmate attributes.) Classes with |ACC_FINAL| but without 
|PermittedSubtypes| behave like traditional final classes.

*Sealing is inherited.* Unless otherwise specified, abstract subtypes of 
sealed types are implicitly sealed, and concrete subtypes are implicitly 
final. This can be reversed by explicitly modifying the subtype with 
|non-final|.

Unsealing a subtype in a hierarchy doesn’t undermine all the benefits of 
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 inheritance, 
requiring sealing to be opted into at all levels. This is widely 
believed to be a source of bugs; it is relatively rare that one actually 
wants a subtype of a sealed type to not be sealed, and in those cases, 
is best to be explicit. Not inheriting would be a simpler rule, 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:

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

*Enforcement.* Both the compiler and JVM should enforce sealing, as they 
both enforce finality today (though from a project-management 
standpoint, 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, some 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 be 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, or 
including an annotation that there exist others that are not listed.)

/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.

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


More information about the amber-spec-experts mailing list