3 Sealed Classes

Sealed classes and interfaces restrict which other classes or interfaces may extend or implement them.

Note:

This is a preview feature, which is a feature whose design, specification, and implementation are complete, but is not permanent, which means that the feature may exist in a different form or not at all in future Java SE releases. To compile and run code that contains preview features, you must specify additional command-line options. See Preview Features.

For background information about sealed classes and interfaces, see JEP 360.

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.

Defining 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

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 { }

Defining 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(); }
}

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