Java 17 Sealed Classes: sealed, non-sealed, final, and permits Explained
Java 15 introduced sealed classes and interfaces as a preview feature, and Java 17 promoted them to a standard feature. “Sealed” has occasionally been translated as “彌封” or “密封” in Chinese material — in this post we stick with the English keyword, sealed. The purpose of sealed is to constrain class inheritance and interface implementation. A Java class has four access levels — public, protected, (package-private), private — but before Java 17 the inheritance story had only two states: unmarked (any class can extend) or final (no class can extend). There was no way to say “only these specific classes may extend me.” That’s the gap sealed was introduced to fill.
Concept overview
Three new keywords come into play: sealed, non-sealed, and permits. You apply sealed to a class or interface to mark it as sealed, and then you must follow it with permits to enumerate which classes or interfaces are allowed to extend or implement it.
Any subclass (or sub-interface) of a sealed type must declare how it handles the sealed state it inherits. Three choices are available. Mark the subclass sealed again to keep the restriction going with its own permits list. Mark it non-sealed to opt out — it becomes a normal class that any other class can extend. Or mark it final, the pre-existing keyword meaning no further subclassing is allowed. You can’t just leave it unmarked; the compiler will complain with:
The class Penguin with a sealed direct superclass or a sealed direct superinterface Bird should be declared either final, sealed, or non-sealed
Here’s how the two types work individually.
Sealed interfaces
A sealed interface uses permits to list which interfaces may extend it or which classes may implement it. An interface that extends a sealed interface must declare itself sealed or non-sealed.
Picking sealed interface again puts you back in the same state as before — you must now list your own allowed extenders / implementers. Picking non-sealed interface turns it back into a regular interface that anyone can extend or implement.
A class that implements a sealed interface must declare itself sealed, non-sealed, or final. The differences are covered in the next section.
Sealed classes
A sealed class uses permits to list which classes may extend it. Any class extending a sealed class must declare itself sealed, non-sealed, or final.
Picking sealed class puts you in the same “enumerate your subclasses” situation again. Picking non-sealed class makes the class a normal class that any other class can extend. Picking final class means no class can extend it at all.
Worked example
None of the classes or interfaces below use public, protected, or private — purely to keep the examples short and easy to read. Real code would use whatever access modifiers make sense.
Sealed interface: Animal
We start with an Animal interface, marked sealed, permitting only Chordata to implement it.
sealed interface Animal permits Chordata {
void eat();
}
One note: a sealed interface’s permits list can include either classes or interfaces. An interface that extends a sealed interface can only be sealed or non-sealed — a final interface wouldn’t mean anything. (Though since Java 8 added default methods to interfaces, you could argue a non-extensible, non-implementable interface still has some use as a container for static constants or default behavior.)
Sealed class: Chordata
Chordata implements the Animal interface’s eat() method and permits only Bird to extend it.
sealed class Chordata implements Animal permits Bird {
public void eat() {
System.out.printf("I'm %s, I'm eating.%n",
getClass().getSimpleName());
}
}
Sealed class: Bird
Next is Bird, which permits only Parrot and Penguin to extend it. Most birds can fly, so we add a fly() method.
sealed class Bird extends Chordata permits Parrot, Penguin {
public void fly() {
System.out.printf("I'm %s, I'm flying.%n",
getClass().getSimpleName());
}
}
Non-sealed class: Parrot
Parrot is marked non-sealed, which turns it back into a regular class any other class can extend. Not every parrot species talks, but most of them sing, so we add a sing() method.
non-sealed class Parrot extends Bird {
public void sing() {
System.out.printf("I'm %s, I'm singing.%n",
getClass().getSimpleName());
}
}
Normal class: GrayParrot
GrayParrot extends Parrot. Because Parrot is non-sealed, it’s effectively a normal class and any class can extend it. African gray parrots are famously talkative, so we add a talk() method.
class GrayParrot extends Parrot {
public void talk() {
System.out.printf("I'm %s, I'm talking.%n",
getClass().getSimpleName());
}
}
Final class: Penguin
Finally, Penguin extends Bird and is marked final, so no class can extend Penguin. Penguins can’t fly, so we override fly() accordingly, and we add a swim() method since they do swim.
final class Penguin extends Bird {
@Override
public void fly() {
System.out.printf("I'm %s, I can't fly.%n",
getClass().getSimpleName());
}
public void swim() {
System.out.printf("I'm %s, I'm swimming.%n",
getClass().getSimpleName());
}
}
Putting it together
var chor = new Chordata();
chor.eat(); // I'm Chordata, I'm eating.
var bird = new Bird();
bird.eat(); // I'm Bird, I'm eating.
bird.fly(); // I'm Bird, I'm flying.
var part = new Parrot();
part.eat(); // I'm Parrot, I'm eating.
part.fly(); // I'm Parrot, I'm flying.
part.sing(); // I'm Parrot, I'm singing.
var gray = new GrayParrot();
gray.eat(); // I'm GrayParrot, I'm eating.
gray.fly(); // I'm GrayParrot, I'm flying.
gray.sing(); // I'm GrayParrot, I'm singing.
gray.talk(); // I'm GrayParrot, I'm talking.
var peng = new Penguin();
peng.eat(); // I'm Penguin, I'm eating.
peng.fly(); // I'm Penguin, I can't fly.
peng.swim(); // I'm Penguin, I'm swimming.