Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

Запечатанные классы

Обычно от класса может наследоваться кто угодно. Но иногда набор вариантов закрыт: платёж бывает только картой или наличными, фигура — только кругом или квадратом. Запечатанный (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, которые разбирают этот тип.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.