Java 15 introduced a preview feature called sealed class and interface. It can be used to restrict the classes or interfaces allowed to extend or implement them. While the most common purpose of inheritance is to reuse code, sometimes inheritance is used to model the possibilities in a domain. In that case, it would be better if we can restrict what subtypes are allowed.
For example there is a shop that sells gadgets. But it only sells certain types of gadgets which are battery pack, cell phone, headphone, and charger. All types of gadgets have common attributes, so we are going to create a base parent class and an interface. Each specific gadget type has its own class that needs to extend the base parent class and implement the interface. However, we only want to limit that only certain gadget types can extend the base parent class and implement the interface.
Creating Sealed Class and Interface
Creating Sealed Interface
To create a sealed interface, you need to add sealed
modifier before the interface
keyword. In addition, you are also required to add permits
keyword after the interface name followed by the list of classes or interfaces permitted to implement or extend the interface.
public sealed interface Warranty permits GlobalWarranty, BatteryPack, CellPhone, Headphone, Charger {
Duration getWarranty();
}
Creating Sealed Class
Creating a sealed class is very similar to creating a sealed interface. The sealed
modifier needs to be put before class
. You also have to add permits
after the class name followed by the list of classes allowed to extend it.
public sealed class Gadget permits BatteryPack, CellPhone, Headphone {
private final UUID id;
private final BigDecimal price;
public Gadget(UUID id, BigDecimal price) {
this.id = id;
this.price = price;
}
public UUID getId() {
return this.id;
}
public BigDecimal getPrice() {
return this.price;
}
}
Extend and Implement Sealed Interface and Class
Each class or interface that listed in permits
is required to extend or implement the sealed class or interface. They must be placed in the same module (if the superclass is in a named module) or in the same package (if the superclass is in the unnamed module). In addition, it must declare a modifier before the class
or interface
keyword. The allowed modifiers and their meanings are:
final
: prevent it from being extended.sealed
: allow further restricted extensions.non-sealed
: open for extension by unknown subclasses.
Below is a class named BatteryPack
that extends the Gadget
class and implements the Warranty
interface above. Because the class is not abstract, it's required to implement getWarranty
method. A final
modifier is added before the class
keyword, which means the class cannot be extended by another class.
public final class BatteryPack extends Gadget implements Warranty {
private final int capacity;
public BatteryPack(UUID id, BigDecimal price, int sensitivity, int capacity) {
super(id, price);
this.capacity = capacity;
}
public int getCapacity() {
return this.capacity;
}
public Duration getWarranty() {
return Duration.ofDays(365);
}
}
The below Headphone
class also extends Gadget
and implements Warranty, but it uses sealed
modifier instead. That means it must declare what classes are permitted to extend it.
public sealed class Headphone extends Gadget implements Warranty permits WiredHeadphone, WirelessHeadphone {
private final int sensitivity;
public Headphone(UUID id, BigDecimal price, int sensitivity) {
super(id, price);
this.sensitivity = sensitivity;
}
public int getSensitivity() {
return this.sensitivity;
}
public Duration getWarranty() {
return Duration.ofDays(365);
}
}
Below are the classes that extend the Headphone
class.
public final class WiredHeadphone extends Headphone {
private final int cableLength;
public WiredHeadphone(UUID id, BigDecimal price, int sensitivity, int cableLength) {
super(id, price, sensitivity);
this.cableLength = cableLength;
}
public int getCableLength() {
return this.cableLength;
}
}
public final class WirelessHeadphone extends Headphone {
private final double range;
public WirelessHeadphone(UUID id, BigDecimal price, int sensitivity, double range) {
super(id, price, sensitivity);
this.range = range;
}
public double getRange() {
return this.range;
}
}
Next is another class that also extends Gadget
and implements Warranty
. It uses non-sealed
modifier instead.
public non-sealed class CellPhone extends Gadget implements Warranty {
private final double displaySize;
public CellPhone(UUID id, BigDecimal price, double displaySize) {
super(id, price);
this.displaySize = displaySize;
}
public double getDisplaySize() {
return this.displaySize;
}
public Duration getWarranty() {
return Duration.ofDays(365);
}
}
Since the modifier of the CellPhone
class is non-sealed
, any class can extend it, like the Smartphone
class below.
public final class Smartphone extends CellPhone {
private final String processor;
public Smartphone(UUID id, BigDecimal price, int sensitivity, String processor) {
super(id, price, sensitivity);
this.processor = processor;
}
public String getProcessor() {
return this.processor;
}
}
Below is an interface named GlobalWarranty
that extends the Warranty
interface above. An interface that extends a sealed interface must declare a modifier before the interface
keyword. Because an interface cannot use final
modifier, the allowed modifiers are sealed
and non-sealed
.
public non-sealed interface GlobalWarranty extends Warranty {
List<String> getCountries();
}
Using Reflection API
java.lang.Class
has the following public methods
java.lang.constant.ClassDesc[] getPermittedSubclasses()
boolean isSealed()
getPermittedSubclasses
returns an array of ClasDesc
whose elements represent the direct subclasses or direct implementation classes permitted to extend or implement the class or interface. isSealed
can be used to check whether it represents a sealed class or interface. Below are the usage examples.
Gadget gadget = new Gadget(UUID.randomUUID(), BigDecimal.valueOf(100));
WiredHeadphone wiredHeadphone = new WiredHeadphone(UUID.randomUUID(), BigDecimal.valueOf(50), 80, 50);
System.out.println(gadget.getClass().isSealed());
System.out.println(wiredHeadphone.getClass().isSealed());
System.out.println(Arrays.toString(gadget.getClass().permittedSubclasses()));
System.out.println(Warranty.class.isSealed());
System.out.println(Arrays.toString(Warranty.class.permittedSubclasses()));
Output:
true
false
[ClassDesc[BatteryPack], ClassDesc[CellPhone], ClassDesc[Headphone]]
true
[ClassDesc[GlobalWarranty], ClassDesc[BatteryPack], ClassDesc[CellPhone], ClassDesc[Headphone], ClassDesc[Charger]]
Compatibility with Record
It's also compatible with another Java 15 preview feature called record, which is a special Java class for defining immutable data. Records are implicitly final which makes the hierarchy more concise. Records can be used to express product types, while sealed classes can be used to express sum types. That combination is referred to as algebraic data types.
public record Charger() implements Warranty {
@Override
public Duration getWarranty() {
return Duration.ofDays(30);
}
}
Summary
There are some important things you need to remember
sealed
modifier can be used to restrict the subclasses and subinterfaces that are permitted to extend or implement a class or interface.The allowed subtypes must be declared after thepermits
clause.- Each permitted subtype is required to implement or extend the sealed class or interface.
- Each permitted subtype must be placed in the same package as its supertypes.
- The implementing subtypes must declare a modifier.
To use this feature, the minimum Java version is 15 with the preview feature enabled.
The source code of this tutorial is also available on GitHub