Sealed classes in Java

1. Introduction

In this tutorial, we’ll discuss a very important introduction in Java language, sealed classes and interfaces.

The enums in Java assist in modeling fixed set of values. But sometimes we need to model fixed kind of values. For example,

interface Vehicle { ... }
final class Car implements Vehicle { ... }
final class Truck implements Vehicle { ... }
final class Bus implements Vehicle { ... }

Here, there are three types of Vehicle. To restrict the kind of Vehicle, we have to restrict the set of subclasses or subinterfaces.

Sealed classes allow to define a closed class hierarchy where the class is not open for extension by arbitrary classes.

Before sealed classes, Java provided limited options to achieve this: either make the class as final or make the class or its constructor package-private, so it can only have subclasses in the same package. One point to note here is that the private constructor approach does not work with interfaces.

Sealed class makes it possible for a superclass to be accessible but not widely extensible. Subclasses here are not constrained to be declared as final or declaring their own state.

Sealed classes and interfaces limit the set of classes or interfaces that are allowed to extend or implement them. Sealed classes were proposed by JEP 360 and delivered in JDK 15 as preview feature. In JDK 16 were still the preview feature and finalized in JDK 17. Sealed classes provide more declarative way than access modifiers to restrict the use of superclass.
Sealed classes do not intend to change the final in Java.

2. Description

To seal a class, you use the sealed modifier in its declaration. Following any extends and implements clauses, you add a permits clause to list the specific classes allowed to extend the sealed class.

For example, the following declaration of Shape specifies three permitted subclasses:

public abstract sealed class Shape
    permits Circle, Rectangle, Square { ... }

Classes listed in the permits clause must be located either within the same module (if the superclass is part of a named module) or within the same package (if the superclass belongs to an unnamed module).

If the permitted subclasses are few and small, it can be convenient to declare them in the same source file as the sealed class. In this case, the sealed class can omit the permits clause, and the Java compiler will automatically infer the permitted subclasses from the declarations within the source file.

For example, if the following code is found in Vehicle.java then the sealed class Vehicle is inferred to have three permitted subclasses:

abstract sealed class Vehicle { ... 
    final class Car extends Vehicle { ... }
    final class Bus extends Vehicle { ... }
    final class Truck extends Vehicle { ... }

Anonymous classes and local classes cannot be permitted subtypes of a sealed class because classes specified by permits must have a canonical name.

When a class is declared as sealed, it implies that it can be have subclasses whereas final implies that a class can not have subclasses. Therefore following combinations are invalid for a class:

  • sealed and final
  • non-sealed and final
  • sealed and non-sealed, because a class can not have restricted and unrestricted subclasses at the same time.

A class, whether sealed or non-sealed, can be abstract and contain abstract members. A sealed class can allow abstract subclasses, provided these subclasses are either sealed or non-sealed, but not final.
Since the extends and permits clauses use class names, a permitted subclass and its sealed superclass must be mutually accessible.

Here is an example from Java documentation:

public abstract sealed class Shape
    permits Circle, Rectangle, Square, WeirdShape { ... }

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 final class Square extends Shape { ... }

public non-sealed class WeirdShape extends Shape { ... }

public non-sealed class WeirdShape extends Shape { ... }

3. Constraints on permitted classes

A sealed class imposes three constraints on its permitted subclasses:

  1. The sealed class and its permitted subclasses must belong to the same module, and, if declared in an unnamed module, to the same package.
  2. Every permitted subclass must directly extend the sealed class.
  3. Every permitted subclass must use a modifier to specify how it propagates the sealing initiated by its superclass:
    final: The subclass cannot be extended further.
    sealed: The subclass can be extended, but only by a specific set of classes.
    non-sealed: A permitted subclass can be declared as non-sealed, making that part of the hierarchy open for extension by any subclasses, known or unknown. A sealed class cannot prevent its permitted subclasses from doing this.

Exactly one of the modifiers final, sealed, and non-sealed must be used by each permitted subclass.

4. Sealed classes and Reflection API

Following public methods were added to java.lang.Class:

  • java.lang.constant.ClassDesc[] permittedSubclasses(): Returns an array of java.lang.constant.ClassDesc objects representing all permitted subclasses of the class if it is sealed. If the class is not sealed, an empty array is returned.
  • boolean isSealed(): Returns true if the specified class or interface is sealed; otherwise, returns false.

5. Sealed interfaces

Same as classes, an interface can be sealed by applying the sealed modifier to the interface. The permits clause is added after the extends clause. For example:

sealed interface Celestial 
    permits Planet, Star, Comet { ... }

final class Planet implements Celestial { ... }
final class Star   implements Celestial { ... }
final class Comet  implements Celestial { ... }

6. Sealing and record classes

You can name a record class in the permits clause of a sealed class or interface. Record classes are implicitly final.

7. Sealed classes in JDK

Following are examples from JDK:

  • java.lang.constant.ClassDesc
  • java.lang.constant.MethodHandleDesc

8. Casting with Sealed classes

The compiler has been enhanced to navigate any sealed hierarchy to check if your cast statements are allowed or not. So if a class is sealed then casting with any disjoint type will give error at compile time. For example:

public sealed interface Shape permits Polygon { }
public non-sealed interface Polygon extends Shape { }
public final class Vehicle { }
public class Toy { }

public void work(Shape s) {
    Vehicle u = (Vehicle) s;  // Error
    Toy t = (Toy) s;          // Permitted

Here, Vehicle u = (Vehicle) s; is not allowed because only Polygon is permitted by Shape.

9. Conclusion

Sealed classes in Java, offer a powerful way to manage and restrict inheritance hierarchies. By explicitly specifying which classes or interfaces can extend or implement a sealed class or interface, developers gain greater control over their code’s architecture, improving maintainability and security. This feature enables more predictable code behavior and helps prevent unauthorized extensions.

Sealed classes are particularly useful in designing APIs and libraries where controlled extension is crucial. By understanding and leveraging sealed classes, developers can create more robust and well-defined class hierarchies, leading to cleaner and more reliable codebases.