3 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 3-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 3-2 Circle.java

public final class Circle extends Shape {
    public float radius;
}

Figure 3-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 3-4 Rectangle.java

public sealed class Rectangle extends Shape permits FilledRectangle {
    public double length, width;
}

Rectangle has a further subclass, FilledRectangle:

Figure 3-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 of Shape: Circle.java, Square.java, and Rectangle.java. In addition, because Rectangle is a sealed class, the compiler also needs access to FilledRectangle.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 is final while Rectangle is sealed and Square is non-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 if Shape 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