Java - Sealed Class and Interface Examples

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 the permits 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