Introduction
Sealed classes were introduced as a preview feature in both Java 15 and 16, and then finalised in Java 17 as JEP 409.
Sealing allows classes and interfaces to define which classes can implement or extend them. This fine-grained inheritance control is useful for precise domain modelling and to improve the security of class libraries. Sealed classes also enable the compiler to check for pattern matching.
Motivation
We use inheritance for modelling hierarchies of related classes. From the first days of Java, it was assumed that code reuse was always a goal of inheritance. So every class was extendable by any number of subclasses. But code reuse is a benefit of inheritance, not necessarily the primary goal.
We want a superclass to be widely accessible because it is an important abstraction in our domain model. However, we might not want the superclass to be widely extensible, because we want to restrict its allowed subclasses. We will still get code reuse within this closed class hierarchy, but not outside it.
There were only a limited number of ways that we could control inheritance, e.g. if we only want a limited number of classes to extend from a superclass.
Let’s say we were developing a Employee
class hierarchy. We want to extend the Employee
class to Programmer
, Secretary
and Manager
. However, we don’t want the Contractor
class to extend from Employee
, because contractors aren’t employees. They don’t get paid leave, promotions, covered parking, etc.
Java only has a limited number options for this: we can either make a class final
, so it has no subclasses; or we can make the class package-private, so it can only have subclasses in the same package.
Sealed Classes
Sealed classes declaratively restrict the set of subclasses in a cleaner way than using access modifiers. They allow us to accurately model class hierarchies that should not be open to casual inheritance. Sealed classes allow a superclass to be widely accessible, but not widely extensible.
The sealed feature adds some new modifiers and clauses to Java. These are sealed
, non-sealed
, and permits
.
A class is sealed by adding the sealed
modifier to its declaration. The permits
clause specifies the classes that are permitted to extend the sealed class. The permits
clause comes after any extends
and implements
clauses.
For example, the following declaration of our Employee
class specifies that only three specific subclasses are permitted:
package com.incusdata.employee.model;
public abstract sealed class Employee
permits Programmer, Secretary, Manager
{ /* controlled extension */ }
The classes specified by permits
must be located near the superclass. These classes are modelled, coded and maintained together, so they shouldn’t be separated.
-
If we use modules, we must put the classes in the same module.
-
If we don’t use modules, then we must put the the sealed class and its direct subclasses in the same package.
-
However, if we don’t want to use modules, we can’t put the superclass in one interface package and the subclasses into a separate implementation package.
Sealed types and their direct subtypes can be generic.
Subclasses
A subclass of a sealed class must specify whether it is sealed
, final
, or open for extension. If it’s open for extension, it must be declared as non-sealed
(yup, a hyphen in a keyword — horrors!).
package com.incusdata.employee.model;
public abstract sealed class Employee
permits Programmer, Secretary, Manager
{ /* controlled extension */ }
public final class Manager extends Employee
{ /* cannot be extended. No more manager types - yay! */ }
public sealed class Programmer extends Employee
permits JavaProgrammer, PythonProgrammer
{ /* controlled extension */ }
public non-sealed class Secretary extends Employee
{ /* can be extended by any number of unknown classes */ }
When we don’t have many permitted subclasses and they are relatively small, we can declare them in the same source file as the sealed class. We can then leave out the permits
clause in the sealed class. The Java compiler will infer the permitted subclasses from the other class declarations in the source file.
For example, if the Employee.java
file contains the following code, then the sealed class Employee
is inferred to have three permitted subclasses:
abstract sealed class Employee { ...
final class Programmer extends Employee { ... }
final class Secretary extends Employee { ... }
final class Manager extends Employee { ... }
}
Sealed Interfaces
An interface can be sealed in the same way as a class, with a fixed set of permitted direct subtypes. With a sealed interface, its direct subtypes can be both interfaces and classes.
The rules are the same for interfaces and classes. All direct subtypes must be listed in the permits
clause, or be in the same source file. The subtypes must all be marked as final
, sealed
, or non-sealed
.
We can implement a sealed interface with a record, which is implicitly final
.
Reflection
Two methods have been added to java.lang.Class
to support sealed classes.
- The
isSealed()
method returnstrue
for a sealed class. - The
getPermittedSubclasses()
method returns the permitted subclasses as an array ofClass
objects.
Summary
Sealed types are fairly straightforward. The key points to remember are:
- A sealed type (class and/or interface) has a fixed set of direct subtypes.
- The direct subtypes of a sealed type must be listed in a permits clause, or, if there is no permits clause, must be in the same source code file.
- The direct subtypes of a sealed type must be final, sealed, or non-sealed.
- Future pattern matching features can carry out exhaustiveness checking with sealed types.
For more technical information on sealed classes, see JEP 409.
Was this post useful? Please share your comments on the blog post, and as always, stay safe and keep learning!