Sealed classes
Normally anyone can inherit from a class. But sometimes the set of variants is closed: a payment is made only by card or in cash, a shape is only a circle or a square. A sealed (sealed) class or interface lists all of its subtypes, and there can be no others.
sealed and permits
After the word permits you list the classes that are allowed to inherit. Each of them must decide what happens next — for example, become final, which closes inheritance from it:
sealed interface Shape permits Circle, Square {}
final class Circle implements Shape {
double radius;
Circle(double radius) {
this.radius = radius;
}
}
final class Square implements Shape {
double side;
Square(double side) {
this.side = side;
}
}
void main() {
Shape shape = new Square(2);
IO.println(shape instanceof Square);
}Output:
trueRecords as variants
Records are implicitly final, so they make great variants. If all subtypes are declared in the same file, you may omit permits — the compiler finds them by itself:
sealed interface Payment {}
record Card(String number) implements Payment {}
record Cash(double amount) implements Payment {}
void main() {
Payment p = new Cash(20);
IO.println(p);
}Output:
Cash[amount=20.0]switch without default
The main benefit of sealed types: the compiler knows all variants. A switch can handle each type, and no default branch is needed:
sealed interface Shape {}
record Circle(double radius) implements Shape {}
record Square(double side) implements Shape {}
double area(Shape shape) {
return switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Square s -> s.side() * s.side();
};
}
void main() {
IO.println(area(new Square(3)));
IO.println(Math.round(area(new Circle(1)) * 100) / 100.0);
}Output:
9.0
3.14If you later add a new variant, say Triangle, this switch stops compiling, and you will not forget to handle it. Such switch statements are covered in more detail in the chapter on pattern matching.
final, sealed and non-sealed
Each permitted subclass must choose one of three modifiers: final — no more subclasses, sealed — it has its own closed list, non-sealed — anyone may inherit further. The last option leaves the "door" open for extension:
sealed abstract class Vehicle permits Car, Truck {}
final class Car extends Vehicle {}
non-sealed class Truck extends Vehicle {}
class FireTruck extends Truck {}
void main() {
Vehicle v = new FireTruck();
IO.println(v instanceof Truck);
}Output:
trueCommon mistake
A class that is not in the permits list cannot inherit a sealed type — even if it is declared in the same file:
sealed interface Shape permits Circle {}
final class Circle implements Shape {}
final class Triangle implements Shape {}
void main() {
}Output:
Main.java:5: error: class is not allowed to extend sealed class: Shape (as it is not listed in its 'permits' clause)
final class Triangle implements Shape {}
^Add the new class to permits — and check every switch that handles this type while you are at it.