Запечатанные классы
Обычно от класса может наследоваться кто угодно. Но иногда набор вариантов закрыт: платёж бывает только картой или наличными, фигура — только кругом или квадратом. Запечатанный (sealed) класс или интерфейс перечисляет всех своих наследников, и других быть не может.
sealed и permits
После слова permits перечисляют классы, которым разрешено наследовать. Каждый из них должен сам решить, что будет дальше, — например, стать final, то есть закрыть наследование от себя:
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);
}Результат:
trueЗаписи как варианты
Записи неявно final, поэтому отлично подходят на роль вариантов. Если все наследники объявлены в том же файле, permits можно не писать — компилятор найдёт их сам:
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);
}Результат:
Cash[amount=20.0]switch без default
Главная польза запечатанных типов: компилятор знает все варианты. В switch можно разобрать каждый тип, и ветка default не понадобится:
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);
}Результат:
9.0
3.14Если позже добавить новый вариант, например Triangle, этот switch перестанет компилироваться, и вы не забудете его обработать. Подробнее о таких switch — в главе о сопоставлении с образцом.
final, sealed и non-sealed
Каждый разрешённый подкласс обязан выбрать один из трёх модификаторов: final — наследников больше нет, sealed — у него свой закрытый список, non-sealed — дальше наследовать может кто угодно. Последний вариант оставляет «дверь» открытой для расширения:
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);
}Результат:
trueЧастая ошибка
Класс, которого нет в списке permits, не может наследовать запечатанный тип — даже если он объявлен в том же файле:
sealed interface Shape permits Circle {}
final class Circle implements Shape {}
final class Triangle implements Shape {}
void main() {
}Результат:
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 {}
^Добавьте новый класс в permits — и заодно проверьте все switch, которые разбирают этот тип.