Java 15 new feature | Sealed Classes and Interfaces
The Sealed Classes concepet is more of related to inheritance and restricting a class to be extended by any arbitrary class. The main goal of behind inheritance was a way to provide code-reusability by extending super-class functionalities into our sub-classes. But we sometime use inheritance concept to describe various possibilities in our system, like
- In a graphics library, the author of a class
Shape
may intend that only particular classes can extendShape
e.g. Circle, Rectangle and Triangle. - In a payment gateway service what kind of payment-method supported by the gateway onli limited to
card
andnet-banking
.
You are trying to restrict the class to be extended which you can do either by using final
keyword in java
but implecation of that would be you can’t have any subclasses or you can use your class without access modifiers and it will be package-private
by default, and will only be accessible within the the same package so untimatily can only be subclassed by the classes of same package.
But there also comes problem like any class can extend you class but your goal was to restrict the class to be used by only perticular set of classes. Here comes Sealed Classes concepet. In java 15
this concept is introduced as a preview feature. 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).center();
} else if (shape instanceof Square) {
return ((Square)shape).center();
}
}
Appending a catch-all else
clause would be contrary to the developer's conviction that the tests are already exhaustive. In addition, the compiler has no ability to save the developer if their conviction turns out to be wrong. Suppose the code above is accidentally edited to omit, say, the instanceof Rectangle
test; no compile-time error will occur. (The omission might be easy to spot with three permitted subclasses, but not with 10 or 20. Even with just three, the code is frustrating to write and tedious to read.)
The ability to reason clearly and conclusively about permitted subclasses will be realized in a future release that supports pattern matching. Instead of inspecting an instance of a sealed class with if
-else
, client code will be able to switch over the instance using type test patterns (JEP 375). This allows the compiler to check that the patterns are exhaustive. For example, given the following code, the compiler will infer that every permitted subclass of Shape
is covered, so no default
clause (or other total pattern) is needed; moreover, the compiler will give an error if any of the three cases are missing:
int getCenter(Shape shape) {
return switch (shape) {
case Circle c -> ... c.center() ...
case Rectangle r -> ... r.length() ...
case Square s -> ... s.side() ...
};
}
A sealed class imposes three constraints on its permitted subclasses (the classes specified by its permits
clause):
- The sealed class and its permitted subclasses must belong to the same module, and, if declared in an unnamed module, the same package.
- Every permitted subclass must directly extend the sealed class.
- Every permitted subclass must choose a modifier to describe how it continues the sealing initiated by its superclass:
- A permitted subclass may be declared
final
to prevent its part of the class hierarchy from being extended further. - A permitted subclass may be declared
sealed
to allow its part of the hierarchy to be extended further than envisaged by its sealed superclass, but in a restricted fashion. - A permitted subclass may be declared
non-sealed
so that its part of the hierarchy reverts to being open for extension by unknown subclasses. (A sealed class cannot prevent its permitted subclasses from doing this.)
As an example of the third constraint, Circle
may be final
while Rectangle
is sealed
and Square
is non-sealed
:
package com.example.geometry;
public abstract sealed class Shape
permits Circle, Rectangle, Square {...}
public final class Circle extends Shape {...}
public sealed class Rectangle extends Shape
permits TransparentRectangle, FilledRectangle {...}
public final class TransparentRectangle extends Rectangle {...}
public final class FilledRectangle extends Rectangle {...}
public non-sealed class Square extends Shape {...}
One and only one of the modifiers final
, sealed
, and non-sealed
must be used by each permitted subclass. It is not possible for a class to be both sealed
(implying subclasses) and final
(implying no subclasses), or both non-sealed
(implying subclasses) and final
(implying no subclasses), or both sealed
(implying restricted subclasses) and non-sealed
(implying unrestricted subclasses).
(The final
modifier can be considered as a strong form of sealing, where extension/implementation is prohibited completely. That is, final
is conceptually equal to sealed
+ a permits
clause which specifies nothing; note that such a permits
clause cannot be written in Java.)
Abstract classes. A class which is sealed
or non-sealed
may be abstract
, and have abstract
members. A sealed
class may permit subclasses which are abstract
(providing they are then sealed
or non-sealed
, rather than final
).
Class accessibility. Because extends
and permits
clauses make use of class names, a permitted subclass and its sealed superclass must be accessible to each other. However, permitted subclasses need not have the same accessibility as each other, or as the sealed class. In particular, a subclass may be less accessible than the sealed class; this means that, in a future release when pattern matching is supported by switches, some users will not be able to exhaustively switch
over the subclasses unless a default
clause (or other total pattern) is used. Java compilers will be encouraged to detect when a user's switch
is not as exhaustive as the user imagined it would be, and customize the error message to recommend a default
clause.
Sealed interfaces
Similar to the story for classes, an interface is sealed by applying the sealed
modifier to the interface. After any extends
clause to specify superinterfaces, the implementing classes and subinterfaces are specified with a permits
clause. For example:
package com.example.expression;
public sealed interface Expr
permits ConstantExpr, PlusExpr, TimesExpr, NegExpr {...}
public final class ConstantExpr implements Expr {...}
public final class PlusExpr implements Expr {...}
public final class TimesExpr implements Expr {...}
public final class NegExpr implements Expr {...}
Sealed classes and Records
Sealed classes work well with records (JEP 384), another preview feature of Java 15. Records are implicitly final
, so a sealed hierarchy with records is slightly more concise than the example above:
package com.example.expression;public sealed interface Expr
permits ConstantExpr, PlusExpr, TimesExpr, NegExpr {...}public record ConstantExpr(int i) implements Expr {...}
public record PlusExpr(Expr a, Expr b) implements Expr {...}
public record TimesExpr(Expr a, Expr b) implements Expr {...}
public record NegExpr(Expr e) implements Expr {...}
The combination of sealed classes and records is sometimes referred to as algebraic data types: Records allow us to express product types, and sealed classes allow us to express sum types.
Java Grammar
NormalClassDeclaration:
{ClassModifier} class TypeIdentifier [TypeParameters]
[Superclass] [Superinterfaces] [PermittedSubclasses] ClassBodyClassModifier:
(one of)
Annotation public protected private
abstract static sealed final non-sealed strictfpPermittedSubclasses:
permits ClassTypeListClassTypeList:
ClassType {, ClassType}
JVM support for sealed classes
The Java Virtual Machine recognizes sealed
classes and interfaces at runtime, and prevents extension by unauthorized subclasses and subinterfaces.
Although sealed
is a class modifier, there is no ACC_SEALED
flag in the ClassFile
structure. Instead, the class
file of a sealed class has a PermittedSubclasses
attribute which implicitly indicates the sealed
modifier and explicitly specifies the permitted subclasses:
PermittedSubclasses_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 number_of_classes;
u2 classes[number_of_classes];
}
The list of permitted subclasses is mandatory — even when the permitted subclasses are inferred by the compiler, those inferred subclasses are explicitly included in the PermittedSubclasses
attribute.
The class
file of a permitted subclass carries no new attributes.
When the JVM attempts to defines a class whose superclass or superinterface has a PermittedSubclasses
attribute, the class being defined must be named by the attribute. Otherwise, an IncompatibleClassChangeError
is thrown.
Reflection API
The following public
methods will be added to java.lang.Class
:
java.lang.constant.ClassDesc[] getPermittedSubclasses()
boolean isSealed()
The method getPermittedSubclasses()
returns an array containing java.lang.constant.ClassDesc
objects representing all the permitted subclasses of the class if it is sealed, and returns an empty array if the class is not sealed.
The method isSealed
returns true if the given class or interface is sealed. (Compare with isEnum
.)
References:
https://jdk.java.net/15/
https://openjdk.java.net/projects/jdk/15/
https://openjdk.java.net/jeps/360