JEP 360: Sealed Classes (Preview)
Summary
Enhance the Java programming language with sealed classes and interfaces. Sealed classes and interfaces restrict which other classes or interfaces may extend or implement them. This is a preview language feature in JDK 15.
Goals
- Allow the author of a class or interface to control which code is responsible for implementing it.
- Provide a more declarative way than access modifiers to restrict the use of a superclass.
- Support future directions in pattern matching by underpinning the exhaustive analysis of patterns.
Non-Goals
- It is not a goal to provide new forms of access control such as "friends".
- It is not a goal to change
final
in any way.
Motivation
In Java, a class hierarchy enables the reuse of code via inheritance: The methods of a superclass can be inherited (and thus reused) by many subclasses. However, the purpose of a class hierarchy is not always to reuse code. Sometimes, its purpose is to model the various possibilities that exist in a domain, such as the kinds of shapes supported by a graphics library or the kinds of loans supported by a financial application. When the class hierarchy is used in this way, restricting the set of subclasses can streamline the modeling.
For example, in a graphics library, the author of a class Shape
may intend that only particular classes can extend Shape
, since much of the library's work involves handling each kind of shape in the appropriate way. The author is interested in the clarity of code that handles known subclasses of Shape
, and not interested in writing code to defend against unknown subclasses of Shape
. Allowing arbitrary classes to extend Shape
, and thus inherit its code for reuse, is not a goal in this case. Unfortunately, Java assumes that code reuse is always a goal: If Shape
can be extended at all, then it can be extended by any number of classes. It would be helpful to relax this assumption so that an author can declare a class hierarchy that is not open for extension by arbitrary classes. Code reuse would still be possible within such a closed class hierarchy, but not beyond.
Java developers are familiar with the idea of restricting the set of subclasses because it often crops up in API design. The language provides limited tools in this area: either make a class final
, so it has zero subclasses, or make a class or its constructor package-private, so it can only have subclasses in the same package. An example of a package-private superclass appears in the JDK:
package java.lang;
abstract class AbstractStringBuilder {...}
public final class StringBuffer extends AbstractStringBuilder {...}
public final class StringBuilder extends AbstractStringBuilder {...}
The package-private approach is useful when the goal is code reuse, such as the subclasses of AbstractStringBuilder
sharing its code for append
. However, the approach is useless when the goal is modeling alternatives, since user code cannot access the key abstraction -- the superclass -- in order to switch
over it. It is not possible to allow users to access the superclass without also allowing them to extend it. (Even within a graphics library that declares Shape
and its subclasses, it would be unfortunate if only one package could access Shape
.)
In summary, it should be possible for a superclass to be widely accessible (since it represents an important abstraction for users) but not widely extensible (since its subclasses should be restricted to those known to the author). Such a superclass should be able to express that it is co-developed with a given set of subclasses, both to document intent for the reader and to allow enforcement by the Java compiler. At the same time, the superclass should not unduly constrain its subclasses by, e.g., forcing them to be final
or preventing them from defining their own state.
Description
A sealed class or interface can be extended or implemented only by those classes and interfaces permitted to do so.
A class is sealed by applying the sealed
modifier to its declaration. Then, after any extends
and implements
clauses, the permits
clause specifies the classes that are permitted to extend the sealed class. For example, the following declaration of Shape
specifies three permitted subclasses:
package com.example.geometry;
public abstract sealed class Shape
permits Circle, Rectangle, Square {...}
The classes specified by permits
must be located near the superclass: either in the same module (if the superclass is in a named module) or in the same package (if the superclass is in the unnamed module). For example, in the following declaration of Shape
, its permitted subclasses are all located in different packages of the same named module:
package com.example.geometry;
public abstract sealed class Shape
permits com.example.polar.Circle,
com.example.quad.Rectangle,
com.example.quad.simple.Square {...}
When the permitted subclasses are small in size and number, it may be convenient to declare them in the same source file as the sealed class. When they are declared in this way, the sealed
class may omit the permits
clause, and the Java compiler will infer the permitted subclasses from the declarations in the source file (which may be auxiliary or nested classes). For example, if the following code is found in Shape.java
, then the sealed class Shape
is inferred to have three permitted subclasses:
package com.example.geometry;
abstract sealed class Shape {...}
... class Circle extends Shape {...}
... class Rectangle extends Shape {...}
... class Square extends Shape {...}
The purpose of sealing a class is to let client code reason clearly and conclusively about all permitted subclasses. The traditional way to reason about subclasses is with an if
-else
chain of instanceof
tests, but analyzing such chains is difficult for the compiler, so it cannot determine that the tests cover all permitted subclasses. For example, the following method would cause a compile-time error because the compiler does not share the developer's conviction that every subclass of Shape
is tested and leads to a return
statement:
int getCenter(Shape shape) {
if (shape instanceof Circle) {
return ... ((Circle)shape).center() ...
} else if (shape instanceof Rectangle) {
return ... ((Rectangle)shape).length() ...
} else if (shape instanceof Square) {
return ... ((Square)shape).side() ...
}
}