Guide to the sealed, permits, and non-sealed modifiers in Java. Learn What are Java Sealed Classes and Sealed Interfaces, and why are they useful.
Tutorial Contents
Overview
Java has introduced the concept of sealed classes and interfaces in Java SE 15 preview. A sealed class or an interface can specify a list of classes or interfaces, which can extend or implement them. Which mens, no other classes or interfaces except for the ones permitted by a sealed class or a sealed interface can be their subtypes.
In this tutorial, we will learn everything about Java Sealing and see how this feature is useful. However, it is important to note that sealed classes is a preview feature in Java SE 15. That means, Java may keep, change, or drop this feature in future releases.
Class Hierarchy
In Java a class can extend another class or implement one or more interfaces. This is called as inheritance. The class which is being extended, or the interface which is being implemented are termed as types and the subclasses or the implementations are called as subtypes. The inheritance serves two major purposes, those are reusability and modelling or type system.
However, traditionally Java only gave importance to the reusability aspect of the inheritance. Because of that, Java started loosing on the type system that is provided by the inheritance. Now that Java had introduced the concept of Sealing Classes it emphasises more on modelling or type system.
Reusability by Hierarchy
Consider a simple case, of an abstract class of Bird
, which has a concrete implementation for the fly
method.
Our second class Eagle
extends the Bird
and inherits the fly method. In other words, the Eagle class does not rewrite the fly method. Instead, it just reuses the method from its parent class.
abstract class Bird {
protected void fly() {
System.out.println("Bird Flying");
}
}
class Eagle extends Bird {
}
Code language: Java (java)
This is why we can call the fly
method on the eagle instance and the super class method gets called.
jshell> eagle.fly();
Bird Flying
Code language: Bash (bash)
This was an example of using hierarchy for reusability. However, the hierarchy in this example, also expresses a type system or a domain model.
Let’s understand that in the next section.
Type System using Hierarchy
In object oriented programming a hierarchical association establishes a type system and relationships between the two classes or interfaces. For example, in the above piece of code the Eagle extends the Bird class. Hence, we can say that an eagle Is-a bird. Or, in other words an eagle is of type bird.
Let’s add Owl class to this system.
class Owl extends Bird{
}
Code language: Java (java)
And now, we can say that, we have a Bird type and both Eagle and Owl are of the Bird type. Or simply, owl Is-a Bird and Eagle Is-a Bird.
Unwanted Subclass Breaks Type System
As stated above, traditionally Java gave more importance to the reusability in hierarchical relationships. But some reusability based relationships breaks the type system.
To understand, let’s add a Bat class to our system. We know that bats can fly, and we also know that the Bird class has a concrete implementation for the fly method. Thus, we will reuse the same method for the bats.
class Bat extends Bird{
}
Code language: Java (java)
As the Bat is extending the Bird class, it can now fly.
jshell> bat.fly();
Bird Flying
Code language: Bash (bash)
Although, this is technically correct. But we just broke the domain type system. Because of the hierarchy we must say a bat Is-a bird. However, a bat is actually an animal, not bird.
Bird bat = new Bat();
Code language: Java (java)
The above statement is technically correct, where states that an instance of Bat is of a Bird type. But it is incorrect in the Domain Models of our system.
This problem arise because, a class couldn’t decide who can subclass it.
Before introduction of Sealed Classes, there were a few workarounds like final classes or package private classes that are used to prevent such unwanted subclasses.
Final Classes
Adding a final modifier on a class prevents it from subclassing. This is a nice way to prevent unwanted modifications to the class behaviour, which can be done by extending it. However, adding a final modifier makes the class completely unavailable for extension.
Package Private Classes
Another way to prevent the unwanted subclassing is to make the superclass a package private. Because of that, only the classes from the same package can subclass it.
However, the problem with this approach is the superclass will be hidden from outside of the package. This scenario is termed as the Class is Extensible but Not Accessible. As a super classes also represents a type they and abstraction it should be accessible widely.
With the above two approaches it is clear that, there was no way to effectively control the subclassing.
Java Sealing
The motive behind bringing the concept of sealing is to make it possible for a superclass to be widely accessible but not widely extensible. It allows authors of a superclass to restrict the class only to known subclasses.
To achieve this Java has introduced three new keywords : sealed
, permits
, and non-sealed
.
Syntax for Sealed Classes and Interfaces
A sealed class or a sealed interface has sealed
modifier to its declaration. Next, after any extends or implementation clauses the permits
clause specifies a comma separated list of permitted sub classes.
public sealed class permits SuperClass permits SubClass1, Subclass2,..{
...
}
Code language: Java (java)
The above syntax tells use the the SuperClass is a sealed class and only the given subclasses can extend it.
Example of Sealed Class
We have already seen an Abstract Bird class that faced unwanted subclassing. Let’s seal the class to permitted subclasses only.
abstract sealed class Bird permits Eagle, Owl{
protected void fly() {
System.out.println("Bird Flying");
}
}
Code language: Java (java)
Now, only Eagle or Owl can extend the Bird. It is important to note that sealing is not limited to abstract classes only. Any class or interface can be sealed.
Example of Sealed Interface
Similarly, we can also seal and interface by providing permitted classes, which can implement it.
sealed interface Nocturnal permits Owl {
}
Code language: Java (java)
Only Owl can implement the Nocturnal interface.
Advantages of Sealed Classes and Interfaces
Next, are some of the most important advantages of having Sealed Classes and Sealed Interfaces.
- First and the foremost advantage is that only the permitted subclasses can extend the sealed superclass.
- It makes it possible for a super class to be Widely Accessible and Not Widely Extensible.
- The introduction of the new modifier and the keyword, helps documenting the intent.
- Sealed superclasses also express that they are co-developed along with the subclasses and are part of the same co-system.
- Allows compilers to enforce the type system on the users of the class.
- Authors of a superclass gets control over the subclasses, hence they can write methods in more restricted way.
Constraints on Permitted Subclasses
So far, our discussion was limited to sealed classes and sealed interfaces. However, we are now going to talk about the restrictions on subclasses or implementations of the sealed superclasses.
- In order to extend a sealed class or to implement a sealed interface a subclass also need to declare its intent.
- When a sealed super class permits a subclass the subclass must explicitly extend the superclass.
- A permitted subclass must be a
final
,sealed
, ornon-sealed
, and it must denote that using the modifiers in declaration.
A permitted subclass withfinal
modifier is closed for extension. On the other hand a permitted subclass withsealed
modifier is open and it must provide its own list of permitted subclasses. Finally, a subclass, which is neither final nor sealed and is freely available for extension must usenon-sealed
modifier.
- Also, in order to add a subclass to permitted list, that subclass and superclass must belong to the same module.
Sealed Classes and Reflection
The Java Reflections API, now supports Sealed classes and Sealed interfaces. To do that, the reflections api has got two new methods isSealed
, and permittedSubclasses
.
In the next example, we have a sealed class and one of its two permitted sub classes, which is non-sealed.
abstract sealed class Bird permits Eagle, Owl {
protected void fly() {
System.out.println("Bird Flying");
}
}
non-sealed class Eagle extends Bird {
}
Code language: Java (java)
Now, we can write some tests based on Java reflection.
Assert.assertFalse(eagle.getClass().isSealed());
Assert.assertTrue(eagle.getClass().getSuperclass().isSealed());
ClassDesc[] permittedSubclasses = eagle.getClass().getSuperclass().permittedSubclasses();
ClassDesc eagleClassDesc = ClassDesc.of(eagle.getClass().getCanonicalName());
Assert.assertTrue(Arrays.asList(permittedSubclasses).contains(eagleClassDesc));
Code language: Java (java)
The example demonstrates use of isSealed
and permittedSubclasses
methods on the classes.
Sealed Classes with Java Record
The Java Records are a simpler way to create Immutable Data Objects. Record classes in Java are implicitly final. Thus, they can be easily used as permitted subclasses.
sealed interface Nocturne permits Bat {
default void wakeUpWhenDark() {
System.out.println("Nocturne is waking up");
}
}
record Bat(long id, String name, int age) implements Nocturne {
@Override
public void wakeUpWhenDark() {
System.out.println("Bat is waking up");
}
}
Code language: Java (java)
The Bat class which is an implementation of the Sealed Nocturne interface is overriding the default method.
Summary
In this tutorial we had a detailed Introduction to Java Sealed Classes and Sealed Interfaces. We understood the two aspects of Java Hierarchy and how sealed classes improves domain modelling, type system, and documentation. Also, we have covered how to write Sealed Class and Sealed Interface, Java Reflection API support for Sealed classes, and How to use Sealed Interfaces with java Records.