Sealed Classes in Java
Inheritance
When you think of what inheritance is used for, the first thing that probably comes to mind is code reuse. This isn't the only reason you use inheritance though. Inheritance is used to model different data structures for a domain. How do you control how those data structures are defined and what can extend them? This is where sealed classes come in.
Preventing Implements and Extends
Before Java 17, there were only two ways to try to control what could extend a class or implement an interface. The first was the access modifier. Classes and interfaces could be marked as package-private to only allow the ability to extend or implement them in the same package.
// package-private interface
interface MyInterface {
}
// package-private class
class MyClass {
}
The second was to mark a class as final. Marking a class as final prevents any classes from being able to extend it.
public class final MyClass extends AnotherClass {
}
Sealed Classes and Interfaces
One of the main reasons for inheritance is code reuse. This isn't the only reason you use inheritance though. Inheritance is also used to model your domain.
Java 17 introduces sealed classes and interfaces. To make a class or interface sealed, you add the sealed modifier to the class or interface declaration. This comes before the interface or class keywords. A sealed class must have a permits clause. A permits clause is where you specify the classes or interfaces that are permitted to extend a class or implement an interface. The permits clause comes after extends and/or implements. Whether you are using an interface or a class, the concept is the same. The class or interface that extends or implements a sealed class must be marked as either final or non-sealed.
The supported modifiers with sealed classes and interfaces are the following:
- final: Cannot be extended any further. This is only for classes.
- sealed: Can only be extended by what is listed in the permits clause. This is used for both interfaces and classes.
- non-sealed: Can be extended by any subclass or interface. Using non-sealed, you cannot prevent what a class or interface extends. This is the same behavior as not using a sealed class.
Sealed Interfaces
A sealed interface must have the sealed modifier and a permits clause. The permitted interfaces that can extend that interface must contain either a sealed modifier with a permits clause or a non-sealed modifier.
// Money.java
public sealed interface Money
permits FiatMoney, PreciousMetal {
}
// FiatMoney.java
public non-sealed interface FiatMoney extends Money {
}
// PreciousMetal.java
public non-sealed interface PreciousMetal extends Money {
}
In this example, only FiatMoney and PreciousMetal would be allowed to extend Money. Since FiatMoney and PreciousMetal are non-sealed, any interface can extend them and any class can implement them. If anything else extends or implements Money, it will result in a compiler error.
// Compiler Error:
// 'Token' is not allowed in the sealed hierarchy
public non-sealed interface Token extends Money {
}
Also, if FiatMoney or PreciousMetal didn't extend Money, this would result in a compiler error. This is shown in a smaller example with the following:
// Money.java
// Compiler error:
// Invalid permits clause: 'Metal' must directly extend 'Money'
public sealed interface Money permits PreciousMetal {
}
// PreciousMetal.java
public interface PreciousMetal {
}
Interfaces can also have classes and enumerations listed in the permits clause. This controls what classes and enumerations can implement that interface.
// PreciousMetal.java
public sealed interface PreciousMetal permits Gold {
}
// Gold.java
public final class Gold implements PreciousMetal {
}
Sealed Classes
Sealed classes are marked with the sealed modifier and have a permits clause just like an interface. Only classes can be in the permits clause since an interface can't extend a class. A more complex example of using sealed classes is the following:
// Money.java
public sealed class Money
permits FiatMoney, PreciousMetal {
}
// FiatMoney.java
public sealed class FiatMoney
extends Money
permits Dollar, Euro {
}
// PreciousMetal.java
public sealed class PreciousMetal
extends Money
permits Gold, Silver {
}
// Dollar.java
public final class Dollar extends FiatMoney {
}
// Euro.java
public final class Euro extends FiatMoney {
}
// Gold.java
public final class Gold extends PreciousMetal {
}
// Silver.java
public non-sealed class Silver extends PreciousMetal {
}
In this example, only FiatMoney and PreciousMetal can extend Money. Both FiatMoney and PreciousMetal are both sealed with their own permits clause. Dollar, Euro, and Gold are marked as final, so they cannot be extended while Silver is marked as non-sealed and can be extended by any class.
Record Types
Record types are implicitly final, so they cannot be marked as non-sealed or have a sealed clause. An example of using a sealed interface with a record type is the following:
// PreciousMetal.java
public sealed interface PreciousMetal permits Gold, Silver {
double price();
}
// Gold.java
public record Gold(double price) implements PreciousMetal {
}
// Silver.java
public record Silver(double price) implements PreciousMetal {
}
Omitting The Permits Clause
You can omit the permits clause in one of two ways. The first is if the classes or interfaces are defined as inner classes or interfaces.
// PreciousMetal.java
public sealed class PreciousMetal {
public final class Gold extends PreciousMetal {
}
public final class Silver extends PreciousMetal {
}
public final class Copper extends PreciousMetal {
}
}
In this example, each inner class is permitted to extend PreciousMetal.
The second way is by defining each in the same file. When doing this, each implementation must be package-private.
// PreciousMetal.java
public sealed class PreciousMetal {
}
final class Gold extends PreciousMetal {
}
final class Silver extends PreciousMetal {
}
final class Copper extends PreciousMetal {
}
In this example, each class is permitted to extend PreciousMetal.
Anonymous Classes and Interfaces
When creating an anonymous class or interface, the compiler will generate a new type for these classes at compile time. Because of this, you cannot create anonymous classes and interfaces if they are sealed.
java.lang.Class
Two methods have been introduced on the java.lang.Class related to sealed classes and interfaces:
- java.lang.constant.ClassDesc[] permittedSubclasses(): Returns an array of the permitted classes and interfaces if it is marked with the sealed modifier. If the class is not sealed, it returns an empty array.
- boolean isSealed(); Returns true if the class or interface is sealed and false if it is not.
Conclusion
Sealed classes are a way to restrict what classes and interfaces can implement or extend an interface or class. Sealed classes must contain a permits clause unless they are all in the same file. When extending a sealed class, the subclass must be marked as either non-sealed, sealed, or final. When extending an interface, the interface must be marked as either non-sealed or sealed with a permits clause.