5 Sealed Classes
Sealed classes and interfaces restrict which other classes or interfaces may extend or implement them.
For background information about sealed classes and interfaces, see JEP 409.
One of the primary purposes of inheritance is code reuse: When you want to create a new class and there is already a class that includes some of the code that you want, you can derive your new class from the existing class. In doing this, you can reuse the fields and methods of the existing class without having to write (and debug) them yourself.
However, what if you want to model the various possibilities that exist in a
domain by defining its entities and determining how these entities should relate to each
other? For example, you're working on a graphics library. You want to determine how your
library should handle common geometric primitives like circles and squares. You've
created a Shape
class that these geometric primitives can extend.
However, you're not interested in allowing any arbitrary class to extend
Shape
; you don't want clients of your library declaring any further
primitives. By sealing a class, you can specify which classes are permitted to extend it
and prevent any other arbitrary class from doing so.
Declaring Sealed Classes
To seal a class, add the sealed
modifier to its
declaration. Then, after any extends
and
implements
clauses, add the permits
clause.
This clause specifies the classes that may extend the sealed class.
For example, the following declaration of Shape
specifies three permitted subclasses, Circle
,
Square
, and Rectangle
:
Figure 5-1 Shape.java
public sealed class Shape
permits Circle, Square, Rectangle {
}
Define the following three permitted subclasses,
Circle
, Square
, and Rectangle
, in
the same module or in the same package as the sealed class:
Figure 5-2 Circle.java
public final class Circle extends Shape {
public float radius;
}
Figure 5-3 Square.java
Square
is a non-sealed class. This type of class is
explained in Constraints on Permitted Subclasses.
public non-sealed class Square extends Shape {
public double side;
}
Figure 5-4 Rectangle.java
public sealed class Rectangle extends Shape permits FilledRectangle {
public double length, width;
}
Rectangle
has a further subclass,
FilledRectangle
:
Figure 5-5 FilledRectangle.java
public final class FilledRectangle extends Rectangle {
public int red, green, blue;
}
Alternatively, you can define permitted subclasses in the same file as
the sealed class. If you do so, then you can omit the permits
clause:
package com.example.geometry;
public sealed class Figure
// The permits clause has been omitted
// as its permitted classes have been
// defined in the same file.
{ }
final class Circle extends Figure {
float radius;
}
non-sealed class Square extends Figure {
float side;
}
sealed class Rectangle extends Figure {
float length, width;
}
final class FilledRectangle extends Rectangle {
int red, green, blue;
}
Constraints on Permitted Subclasses
Permitted subclasses have the following constraints:
-
They must be accessible by the sealed class at compile time.
For example, to compile
Shape.java
, the compiler must be able to access all of the permitted classes ofShape
:Circle.java
,Square.java
, andRectangle.java
. In addition, becauseRectangle
is a sealed class, the compiler also needs access toFilledRectangle.java
. -
They must directly extend the sealed class.
-
They must have exactly one of the following modifiers to describe how it continues the sealing initiated by its superclass:
-
final
: Cannot be extended further -
sealed
: Can only be extended by its permitted subclasses -
non-sealed
: Can be extended by unknown subclasses; a sealed class cannot prevent its permitted subclasses from doing this
For example, the permitted subclasses of
Shape
demonstrate each of these three modifiers:Circle
isfinal
whileRectangle
is sealed andSquare
isnon-sealed
. -
-
They must be in the same module as the sealed class (if the sealed class is in a named module) or in the same package (if the sealed class is in the unnamed module, as in the
Shape.java
example).For example, in the following declaration of
com.example.graphics.Shape
, its permitted subclasses are all in different packages. This example will compile only ifShape
and all of its permitted subclasses are in the same named module.package com.example.graphics; public sealed class Shape permits com.example.polar.Circle, com.example.quad.Rectangle, com.example.quad.simple.Square { }
Declaring Sealed Interfaces
Like sealed classes, to seal an interface, add the
sealed
modifier to its declaration. Then, after any
extends
clause, add the permits
clause, which
specifies the classes that can implement the sealed interface and the interfaces
that can extend the sealed interface.
The following example declares a sealed interface named Expr
. Only
the classes ConstantExpr
, PlusExpr
,
TimesExpr
, and NegExpr
may implement it:
package com.example.expressions;
public class TestExpressions {
public static void main(String[] args) {
// (6 + 7) * -8
System.out.println(
new TimesExpr(
new PlusExpr(new ConstantExpr(6), new ConstantExpr(7)),
new NegExpr(new ConstantExpr(8))
).eval());
}
}
sealed interface Expr
permits ConstantExpr, PlusExpr, TimesExpr, NegExpr {
public int eval();
}
final class ConstantExpr implements Expr {
int i;
ConstantExpr(int i) { this.i = i; }
public int eval() { return i; }
}
final class PlusExpr implements Expr {
Expr a, b;
PlusExpr(Expr a, Expr b) { this.a = a; this.b = b; }
public int eval() { return a.eval() + b.eval(); }
}
final class TimesExpr implements Expr {
Expr a, b;
TimesExpr(Expr a, Expr b) { this.a = a; this.b = b; }
public int eval() { return a.eval() * b.eval(); }
}
final class NegExpr implements Expr {
Expr e;
NegExpr(Expr e) { this.e = e; }
public int eval() { return -e.eval(); }
}
Record Classes as Permitted Subclasses
You can name a record class in the permits
clause of a
sealed class or interface. See Record Classes for more information.
Record classes are implicitly final
, so you can
implement the previous example with record classes instead of ordinary classes:
package com.example.records.expressions;
public class TestExpressions {
public static void main(String[] args) {
// (6 + 7) * -8
System.out.println(
new TimesExpr(
new PlusExpr(new ConstantExpr(6), new ConstantExpr(7)),
new NegExpr(new ConstantExpr(8))
).eval());
}
}
sealed interface Expr
permits ConstantExpr, PlusExpr, TimesExpr, NegExpr {
public int eval();
}
record ConstantExpr(int i) implements Expr {
public int eval() { return i(); }
}
record PlusExpr(Expr a, Expr b) implements Expr {
public int eval() { return a.eval() + b.eval(); }
}
record TimesExpr(Expr a, Expr b) implements Expr {
public int eval() { return a.eval() * b.eval(); }
}
record NegExpr(Expr e) implements Expr {
public int eval() { return -e.eval(); }
}
Narrowing Reference Conversion and Disjoint Types
Narrowing reference conversion is one of the conversions used in type checking cast
expressions. It enables an expression of a reference type
S
to be treated as an expression of a
different reference type T
, where
S
is not a subtype of
T
. A narrowing reference conversion may
require a test at run time to validate that a value of type
S
is a legitimate value of type
T
. However, there are restrictions that
prohibit conversion between certain pairs of types when it can be statically proven
that no value can be of both types.
Consider the following example:
public interface Polygon { }
public class Rectangle implements Polygon { }
public void work(Rectangle r) {
Polygon p = (Polygon) r;
}
The cast expression Polygon p = (Polygon) r
is allowed
because it's possible that the Rectangle
value r
could be of type Polygon
; Rectangle
is a subtype
of Polygon
. However, consider this example:
public interface Polygon { }
public class Triangle { }
public void work(Triangle t) {
Polygon p = (Polygon) t;
}
Even though the class Triangle
and the interface
Polygon
are unrelated, the cast expression Polygon p =
(Polygon) t
is also allowed because at run time these types could be
related. A developer could declare the following class:
class MeshElement extends Triangle implements Polygon { }
However, there are cases where the compiler can deduce that there are no values (other than the null reference) shared between two types; these types are considered disjoint. For example:
public interface Polygon { }
public final class UtahTeapot { }
public void work(UtahTeapot u) {
Polygon p = (Polygon) u; // Error: The cast can never succeed as
// UtahTeapot and Polygon are disjoint
}
Because the class UtahTeapot
is final
,
it's impossible for a class to be a descendant of both Polygon
and
UtahTeapot
. Therefore, Polygon
and
UtahTeapot
are disjoint, and the cast statement Polygon
p = (Polygon) u
isn't allowed.
The compiler has been enhanced to navigate any sealed hierarchy to check if your cast statements are allowed. For example:
public sealed interface Shape permits Polygon { }
public non-sealed interface Polygon extends Shape { }
public final class UtahTeapot { }
public class Ring { }
public void work(Shape s) {
UtahTeapot u = (UtahTeapot) s; // Error
Ring r = (Ring) s; // Permitted
}
The first cast statement UtahTeapot u = (UtahTeapot) s
isn't allowed; a Shape
can only be a Polygon
because Shape
is sealed
. However, as
Polygon
is non-sealed
, it can be extended.
However, no potential subtype of Polygon
can extend
UtahTeapot
as UtahTeapot
is
final
. Therefore, it's impossible for a Shape
to be a UtahTeapot
.
In contrast, the second cast statement Ring r = (Ring) s
is allowed; it's possible for a Shape
to be a Ring
because Ring
is not a final
class.
APIs Related to Sealed Classes and Interfaces
The class java.lang.Class has two new methods related to sealed classes and interfaces:
- java.lang.constant.ClassDesc[] permittedSubclasses(): Returns an array containing java.lang.constant.ClassDesc objects representing all the permitted subclasses of the class if it is sealed; returns an empty array if the class is not sealed
- boolean isSealed(): Returns true if the given class or interface is sealed; returns false otherwise