Sealed Classes and Sealed Interfaces

Introduction

Sealed classes and sealed interfaces in Java restrict which other classes or interfaces may extend or implement them. Sealed classes were first part of Java 15 (JEP 360) and Java 16 (JEP 397) as preview features and were added to the language in Java 17. In this post, we will explore what Sealed Classes and Sealed Interfaces in Java are and what it solves.

What are Sealed Classes (and Sealed Interfaces) in Java

A sealed class or a sealed interface restricts which other classes or interfaces can extend or implement it.

Normally, when we have a public interface, any number of classes can implement that interface (or any number of interfaces can extend that interface). If the interface is package-private, the implementing classes of that interface (and the extending interfaces) should be in the same package. If the interface is private, the implementing classes and extending interfaces would be in the same source file (source file having the private interface). The same applies to a class (or an abstract class) as well.

But with the ability to seal classes or interfaces, we (the class or interface author) can control which classes can extend or implement them. This approach is declarative than using access modifiers (as we will see).

Before we dive into the details of sealed classes and sealed interfaces, let us first look at why we need them at all.

Why do we need a sealed class or a sealed interface

When we define an enum, we create enum constants which tells us all the possible instances of the enum type. In the below Color enum, we know there are three instances (three colors) viz., red, blue and green. Other than those, we cannot have any other Color.

public enum Color {
    RED("red"),
    BLUE("blue"),
    GREEN("green");

    private final String color;
        
    Color(String color) {
        this.color = color;
    }
        
    public String getColor() {
        return color;
    }
}

But we cannot model a class or an interface like that. We cannot model a fixed set of kinds of values.

Let us say we have a Shape (say an abstract) class and two implementations of it, viz., Rectangle and a Circle.

public abstract class Shape {}

public final class Rectangle extends Shape {}

public final class Circle extends Shape {}

Can we guarantee that there will be exactly two implementations of the Shape class? – that there will be only two types of shapes? Imagine this code is part of a library and so anyone can write a class which extends the Shape class.

With this, we are facing two problems:

  1. Unable to clearly articulate the business domain intent that a Shape can have only a fixed number of subtypes.
  2. Since the class (inheritance) hierarchy is not closed, any code which handles the subclasses of Shape has to deal with a default case of handling unknown subtypes of Shape.

As shown below, when the library has to handle different subclasses of Shape, it always needs to have a guard to handle an unknown or unrecognized subclass. This manifests as the last else condition.

public void handleShape(Shape shape) {
    if (shape instanceof Rectangle) {
        // handle Rectangle instance
    } else if (shape instanceof Circle) {
        // handle Circle instance
    } else {
        throw new RuntimeException("Unrecognized type of shape: " + shape.getClass().getCanonicalName());
    }
}

If you use the new Switch expression and pattern matching capabilities (added in Java 17), then we can write the above using switch expression as,

switch (shape) {
    case Rectangle r -> {
        //do something with the Rectangle instance r
        System.out.println(r);
    }
    case Circle c -> {
        //do something with the Circle instance c
        System.out.println(c);
    }
    default -> throw new RuntimeException("Unrecognized type of shape: " + shape.getClass().getCanonicalName());
}

Since we cannot prevent creating a new class which extends Shape, the compiler forces to include the default block to handle shapes of other types.

Restricting extension of a class (or an interface)

The objectives are now clear: We need a way to communicate the intent that a given class or interface will only have a certain number of subtypes.

We need a way to create a class hierarchy that is not open for extension by arbitrary classes.

Before Java 17 introduced Sealed classes and interfaces, we had only three ways (2nd and 3rd are similar) to achieve this and all the approaches have some problems.

  1. Making the class final
    1. If we made a class final, then no class can extend that class (zero subclasses). This clearly wouldn’t help us.
  2. Make the class private
    1. This works only if the class is not a top-level class.
  3. Make the class constructor package-private
    1. This means that all the subclasses must be in the same java package.

The third approach is used in the JDK library. The classes StringBuilder and StringBuffer extend an abstract package-private class AbstractStringBuilder.

There are two main problems with the third approach.

  1. It forces all the subtypes to be in the same package.
  2. It hides the supertype, which is the main abstraction.

If we apply it to our Shape class example, class Shape will be package-private and hence we can control the number of subclasses of it. But since it won’t be visible to the users of the library, they cannot use it as the common type. We miss out on polymorphism and any code which wants to switch over the subtypes of it has to be in the same package within the library.

If Shape had been an interface, since it is package-private, we had lost the original intention of creating an interface (programming to an interface).

With this, we refine the requirement slightly.

  1. We need to restrict the subtypes of a type (this clearly indicates the business intent for the reader)
  2. The superclass must be accessible, but we shouldn’t allow others to extend it (or implement in case of an interface). In other words, the superclass should be accessible but not extensible.

Sealing a class – Mechanism

Let us now look at how to seal a class. In our above example, to seal the Shape class, we add the sealed modifier to the class declaration. Then, as we do normally, we can list the list of interfaces it implements or the class it extends. Neither of these is applicable to the Shape class, so we can skip it. At last, we add the permits clause and specify the list of classes that are allowed to extend the Shape class. With this, the Shape class becomes a sealed class.

public sealed abstract class Shape permits Rectangle, Circle {

}

We can also change the order of abstract and sealed in the class declaration.

public abstract sealed class Shape permits Rectangle, Circle {

}

Once we’ve sealed the Shape class, we cannot create a new direct subclass of class Shape. (You might wonder why am I emphasizing on direct. We’ll see in a moment).

Constraints on Subclass Positioning

There are some constraints on where the permitted classes can be. This depends on the superclass:

  • If the superclass is in a named module, then all the permitted subclasses must be in the same module.
  • If the superclass is not part of the module system, it will be considered to be an unnamed module. In this class, all the subclasses must be in the same package.

Unnamed module

In the above example, if we don’t have a module system for the class, then all the three classes must be in the same package.

If the superclass Shape is in package com.javadevcentral.sealed, then the subclasses Rectangle and Circle must also be in the same package.

If we move Rectangle class to a different package (say com.javadevcentral.sealed.quad), then we get the below error in the Shape class.

Class is not allowed to extend sealed class from another package

All the classes in the same module

If we have our classes in a Java module and say we have the module-info.java in the root, as shown below.

module ShapesLibrary {
}

Then we can have the subclasses in different Java packages. This works because all the classes (the superclass and the subclasses) are part of the same module.

In Shape.java, we have,

package com.javadevcentral.sealed;

import com.javadevcentral.sealed.quad.Rectangle;

public abstract sealed class Shape permits Rectangle, Circle {

}

The Circle class is in the same package as the Shape class, but Rectangle is in a different package.

package com.javadevcentral.sealed.quad;

import com.javadevcentral.sealed.Shape;

public final class Rectangle extends Shape {
}

Shown below is the overall directory structure.

src
├── com
│   └── javadevcentral
│       ├── sealed
│       │   ├── Circle.java
│       │   ├── Shape.java
│       │   ├── quad
│       │   │   └── Rectangle.java
└── module-info.java

If we remove the module-info.java, we would get the same error as before when the Rectangle class was in a different package (unnamed module example).

All the classes in the same source file

If it would be possible for us to have all the permitted subclasses in the same source file along with the sealed superclass, then we can omit the permits clause. The Java compiler will infer the list of permitted subclasses, which are all the subclasses in the same source file (top-level and member classes in the same compilation unit).

public class SealedInSameSourceFile {
    private sealed abstract class Shape {
    }
        
    private final class Rectangle extends Shape {
    }
        
    private final class Circle extends Shape {
    }
}

When we have the above hierarchy, classes Rectangle and Circle would be the permitted subclasses of the sealed class Shape.

We cannot combine these two approaches. The compiler will not infer the list of permitted subclasses if we have a permits clause. If we provide a permits clause, it should include all the subclasses. As shown below, if we include only Rectangle, it will show an error in the Circle class declaration.

public class SealedInSameSourceFile {
    private sealed abstract class Shape permits Rectangle {
    }

    private final class Rectangle extends Shape {
    }

    //Error: 'Circle' is not allowed in the sealed hierarchy
    private final class Circle extends Shape {
    }
}

Modifier of permitted subclasses

Each of the permitted subclasses must specify a modifier to describe how it propagates the sealing that was initiated by its superclass. Each must have one of the three modifiers: final, sealed or non-sealed.

  • If the subclass wants to stop its part of the class hierarchy from being extended, it should be declared with the final modifier.
  • If the subclass will allow its part of the hierarchy to be extended further, then it will have the sealed modifier.
  • If the subclass wants to open up its part of the hierarchy for extension by any number of subclasses, it should specify the non-sealed modifier.

The modifiers final/sealed/non-sealed on a subclass are from the most restrictive to the least restrictive.

  • The final modifier (as we know) stops the inheritance hierarchy at this class (implies no subclasses). There can be no class which can extend this subclass.
  • With sealed, here the subclass again specifies the list of permitted subclass. The subclass is open for extension but restricts the list of classes that can do so (implies restricted subclasses).
  • Finally, the non-sealed on a subclass says it doesn’t restrict which classes can extend the subclass (implies unrestricted subclasses).

The non-sealed modifier on a subclass might sound non-intuitive and it might appear to defeat the entire purpose of sealing the superclass. We started with a base class which we sealed to restrict the classes which can extend it. But one of the subclasses can open up the hierarchy (its part of the hierarchy) again and hence we can have any number of classes extending this particular subclass. We’ll explore why this is allowed with another (real-world) example.

A real life example for sealing classes

Let us now look at another example. Imagine an application where we have a base class for Vehicle. In our application, there can be many subtypes of vehicles viz., LandVehicle, AerospaceVehicle, Watercraft, and AmphibiousVehicle (vehicle usable on land as well as on or under water). Further, there can be two subtypes of LandVehicleAutomobile and Motorcycle.

  • We don’t want any other direct subtypes (subclasses) of Vehicle and hence we will make it a sealed class.
  • A LandVehicle has two known subtypes and we don’t want any more subtypes of it and so we will make it sealed. We will make the two subtypes of LandVehicle final.
  • Similarly, we don’t want any subtypes for AerospaceVehicle and Watercraft. So, we will make them final as well.
  • Let’s say for the AmphibiousVehicle type, others can design different types of amphibious vehicles. We will thus allow users of this application/library to subclass AmphibiousVehicle class. Since we cannot know the subclasses of it, we will make it non-sealed. This will open up the hierarchy at this level (anyone can create a class which extends AmphibiousVehicle).
public abstract sealed class Vehicle permits LandVehicle, AerospaceVehicle, 
        Watercraft, AmphibiousVehicle {
}

public sealed class LandVehicle extends Vehicle permits Automobile, MotorCycle {
}

public final class AerospaceVehicle extends Vehicle {
}

public final class Watercraft extends Vehicle {
}

public non-sealed class AmphibiousVehicle extends Vehicle {
}

public final class Automobile extends LandVehicle {
}

public final class MotorCycle extends LandVehicle {
}

The class hierarchy for this example is shown below.

SealedClassHierarchy
SealedClassHierarchy

The subtypes of AmphibiousVehicle aren’t part of the permitted subclasses. Hence, they need not have any of the modifiers a permitted subclass must have (final/sealed/non-sealed).

Why a subclass of a sealed class is allowed to be ‘non-sealed’

Let’s return to the earlier question on allowing a subclass of a sealed class to be non-sealed. From the above example, we understand it is sometimes useful (and even necessary) to allow one or more non-sealed subclasses somewhere in the hierarchy. The key point is we don’t open up the entire hierarchy. The top-level hierarchy at the base class level (Vehicle in this example) is still sealed.

Irrespective of the fact that the users can create any number of subclasses of the AmphibiousVehicle type, given a Vehicle type, we still would know for sure that there can only be four subtypes of it. Any subclass of AmphibiousVehicle is still is a AmphibiousVehicle. So, if we have a piece of code that needs to handle all subtypes of Vehicle will still know all the immediate subtypes and that should be enough.

Going back to Shape example where we have a switch expression with one case for each type. If we were to have a similar thing for the Vehicle class, we would have:

public void handleVehicle(Vehicle vehicle) {
    switch (vehicle) {
        case LandVehicle landVehicle -> {
        }
    case AerospaceVehicle aerospaceVehicle -> {
    }
    case Watercraft watercraft -> {
    }
    case AmphibiousVehicle amphibiousVehicle -> {
    }
       // no need of a default block
    }
 }

All user-created subclasses of AmphibiousVehicle class (which are outside of our control) will be covered by the last case. Hence, the switching over the base class is still exhaustive and we don’t need a default block for the switch here.

A note on class accessibility

When using sealed classes, the superclass specifies the list of permitted subclasses and the subclass extends or implements the supertype. So, these two must be accessible to each other. But one or more of the permitted subclasses can have a lesser level of accessibility. This will impact the pattern matching supported by switches (still a preview feature as of JDK 20) as it may not be able to exhaustively switch over the subclasses. Hence, the compiler will force us to use a default clause in such cases.

In the above example, let us make Watercraft class as package-private. If the handleVehicle method can access Watercraft class (and all other subtypes), then it will continue to work fine. But if the class which has the handleVehicle method is in a different package, then it will not have access to the Watercraft subclass and this will affect the switch expression. We will be forced to include the default clause as shown below (as we would not be able to switch over all the subclasses).

public void handleVehicle(Vehicle vehicle) {
    switch (vehicle) {
        case LandVehicle landVehicle -> {
        }
        case AerospaceVehicle aerospaceVehicle -> {
        }
        case AmphibiousVehicle amphibiousVehicle -> {
        }
        default -> throw new IllegalStateException("Unexpected value: " + vehicle);
    }
}

Usage of Sealed classes in the JDK

Earlier we saw that AbstractStringBuilder class is package-private (which is the base class for StringBuilder and StringBuffer classes). With the introduction of Sealed Class feature, they’ve rewritten it i.e., AbstractStringBuilder has become a sealed class permitting only StringBuilder and StringBuffer as subclasses.

abstract sealed class AbstractStringBuilder implements Appendable, CharSequence
    permits StringBuilder, StringBuffer {...}

Since sealed classes and interfaces weren’t there when Java was released, we’ve missed an opportunity to use it for the Throwable class. Error and Exception were the only two subclasses that Throwable was meant to have. But since they couldn’t articulate or control this expectation in code, there are a now lot of other direct subclasses of Throwable.

Sealed Interfaces

A sealed interface will be similar to the sealed class we’ve learnt. One thing to notice is that, unlike a class, an interface can have subinterfaces, i.e., other interfaces extending this interface.

We can seal an interface by adding the sealed modifier to the interface. We add permits clause after the extends clause which lists the superinterfaces (of this interface) if any. The permits clause will specify the implementing classes and other subinterfaces of this interface, i.e., classes which are allowed to implement this interface and other interfaces which can extend this.

An example from the java.lang.foreign package (which provides low-level access to memory and functions) is shown below.

public sealed interface Addressable permits MemorySegment, MemoryAddress, 
    VaList { .... }

public sealed interface MemoryAddress extends Addressable 
    permits MemoryAddressImpl { .... }

All these interfaces and classes are part of the preview feature as of Java 19.

The Addressable is a sealed interface, and it permits only three other interfaces. Thus, no other class or interface can implement or extend this sealed interface.

The MemoryAddress interface extends the Addressable interface and allows only one class to implement it.

Unlike a class, it doesn’t make sense for an interface to be final (you cannot make an interface final). Hence, any subinterface of the hierarchy must either be sealed or non-sealed.

Sealing and record classes

Sealed classes work well with record classes. A record class is implicitly final, so having a record as subclass of a sealed interface is more concise.

public sealed interface ValueProvider<T> permits StringProvider {
    T value();
}

public record StringProvider(String value) implements ValueProvider<String> {
}

Sealed classes – Others rules and possible errors

Let us now look at a few of the basic rules around sealed classes and interfaces.

A Sealed Class should have at least one subclass

If we mark a class which doesn’t have any subclass as sealed, then we will get the below error.

Sealed class must have subclasses

A Sealed Class should list all subclasses

Say we have a class (Shape) and one subclass of that class (Circle) and we mark the Shape class as sealed as shown below.

public sealed class Shape { }

public class Circle extends Shape { }

We will get two errors in each class because we have made the Shape class as sealed, but it is missing the permits clause.

Error in class Shape:
Sealed class permits clause must contain all subclasses

Error in class Circle:
'Circle' is not allowed in the sealed hierarchy

A subclass must be final, sealed or non-sealed

After adding permits clause to the Shape class, now, since the subclass Circle is missing one of the modifiers final or sealed or non-sealed, it will give us the following error.

Error in class Shape: 
All sealed class subclasses must either be final, sealed or non-sealed

Error in class Circle:
sealed, non-sealed or final modifiers expected

To fix this, we must add one of the expected modifiers to the Circle class.

Permitted subclasses cannot be Anonymous or Local classes

Since we have to list the permitted subclasses in the permits clause, all subclasses must have a canonical name. Hence, we cannot have an anonymous class or a local class as a permitted subtype of a sealed class.

Permits clause cannot be empty

As we’ve seen, if a subclass is final, then it stops its part of the class hierarchy from being extended further. But we cannot make a subclass as sealed with an empty list of permitted subclasses in the permits clause. This is not allowed.

Also, we cannot have a permits clause on a class which doesn’t have the sealed modifier.

Conclusion

In this post, we learnt about the sealed classes and sealed interface in Java. When we seal a class, we explicitly specify the classes (subclasses) which extend it. The same for a sealed interface as well i.e., we list the classes which implement the sealed interface and/or the interfaces which extend it.

References

Leave a Reply