Sealed Classes — Запечатанные классы
Sealed classes и interfaces (Java 17+) позволяют контролировать, кто может наследовать ваш класс. Зачем нужны, как работают permits, pattern matching и сравнение с final
Проблема: открытая иерархия
Представьте, что вы создаёте систему оплаты. У вас есть интерфейс Payment, и вы знаете три способа оплаты: карта, наличные, перевод.
public interface Payment {
double getAmount();
}
public class CardPayment implements Payment {
private final String cardNumber;
private final double amount;
public CardPayment(String cardNumber, double amount) {
this.cardNumber = cardNumber;
this.amount = amount;
}
@Override
public double getAmount() {
return amount;
}
public String getCardNumber() {
return cardNumber;
}
}
public class CashPayment implements Payment {
private final double amount;
public CashPayment(double amount) {
this.amount = amount;
}
@Override
public double getAmount() {
return amount;
}
}
public class TransferPayment implements Payment {
private final String accountNumber;
private final double amount;
public TransferPayment(String accountNumber, double amount) {
this.accountNumber = accountNumber;
this.amount = amount;
}
@Override
public double getAmount() {
return amount;
}
public String getAccountNumber() {
return accountNumber;
}
}
Проблема в том, что кто угодно может создать новый класс, реализующий Payment. Ваш код, который обрабатывает только три известных типа,uddenly может столкнуться с неизвестным типом оплаты.
// Кто-то другой добавил в библиотеку:
public class CryptoPayment implements Payment {
private final String walletAddress;
private final double amount;
public CryptoPayment(String walletAddress, double amount) {
this.walletAddress = walletAddress;
this.amount = amount;
}
@Override
public double getAmount() {
return amount;
}
public String getWalletAddress() {
return walletAddress;
}
}
Теперь ваш switch по типам оплаты не покрывает все случаи, и программа может работать некорректно.
Решение: sealed class
Ключевое слово sealed позволяет явно указать, какие классы могут наследовать ваш класс или реализовывать ваш интерфейс:
public sealed interface Payment
permits CardPayment, CashPayment, TransferPayment {
double getAmount();
}
public final class CardPayment implements Payment {
private final String cardNumber;
private final double amount;
public CardPayment(String cardNumber, double amount) {
this.cardNumber = cardNumber;
this.amount = amount;
}
@Override
public double getAmount() {
return amount;
}
public String getCardNumber() {
return cardNumber;
}
}
public final class CashPayment implements Payment {
private final double amount;
public CashPayment(double amount) {
this.amount = amount;
}
@Override
public double getAmount() {
return amount;
}
}
public final class TransferPayment implements Payment {
private final String accountNumber;
private final double amount;
public TransferPayment(String accountNumber, double amount) {
this.accountNumber = accountNumber;
this.amount = amount;
}
@Override
public double getAmount() {
return amount;
}
public String getAccountNumber() {
return accountNumber;
}
}
Теперь компилятор гарантирует, что только эти три класса могут реализовывать Payment. Если кто-то попытается создать CryptoPayment implements Payment — компилятор выдаст ошибку.
Аналогия: Sealed class — это как VIP-клуб. Только те, кто в списке приглашённых (permits), могут войти. Все остальные — компилятор не пустит.
Модификаторы permitted classes
Каждый класс из списка permits должен иметь один из трёх модификаторов:
final— класс не может быть наследован дальшеsealed— класс сам является запечатанным и указывает своих наследниковnon-sealed— класс открыт для наследования (любой может наследовать)
public sealed interface Shape
permits Circle, Rectangle, Triangle {
double area();
}
public final class Circle implements Shape {
private final double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
public double getRadius() {
return radius;
}
}
public sealed class Rectangle implements Shape
permits Square, NonSquareRectangle {
protected final double width;
protected final double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double area() {
return width * height;
}
}
public final class Square extends Rectangle {
public Square(double side) {
super(side, side);
}
}
public final class NonSquareRectangle extends Rectangle {
public NonSquareRectangle(double width, double height) {
super(width, height);
}
}
public final class Triangle implements Shape {
private final double base;
private final double height;
public Triangle(double base, double height) {
this.base = base;
this.height = height;
}
@Override
public double area() {
return 0.5 * base * height;
}
}
Здесь:
Shape— sealed interface, permits: Circle, Rectangle, TriangleCircle— final, нельзя наследоватьRectangle— sealed, permits: Square, NonSquareRectangle (иерархия на 2 уровня)Triangle— final, нельзя наследовать
Зачем нужны sealed classes?
Sealed classes решают несколько важных задач:
- Контроль иерархии — вы точно знаете, какие типы возможны
- Безопасность — никто не добавит неизвестный подтип
- Pattern matching — компилятор может проверить полноту switch
- Документирование — permits явно показывает все варианты
- Производительность — JIT-компилятор может оптимизировать, зная полную иерархию
Pattern matching с sealed classes
Sealed classes идеально сочетаются с pattern matching в switch. Компилятор знает все возможные типы и может:
- Проверить, что все типы обработаны
- Предупредить, если забыли case
- Автоматически привести тип
public sealed interface Payment
permits CardPayment, CashPayment, TransferPayment {
double getAmount();
}
public final class CardPayment implements Payment {
private final String cardNumber;
private final double amount;
public CardPayment(String cardNumber, double amount) {
this.cardNumber = cardNumber;
this.amount = amount;
}
@Override
public double getAmount() {
return amount;
}
public String getCardNumber() {
return cardNumber;
}
}
public final class CashPayment implements Payment {
private final double amount;
public CashPayment(double amount) {
this.amount = amount;
}
@Override
public double getAmount() {
return amount;
}
}
public final class TransferPayment implements Payment {
private final String accountNumber;
private final double amount;
public TransferPayment(String accountNumber, double amount) {
this.accountNumber = accountNumber;
this.amount = amount;
}
@Override
public double getAmount() {
return amount;
}
public String getAccountNumber() {
return accountNumber;
}
}
public class PaymentProcessor {
public static String process(Payment payment) {
return switch (payment) {
case CardPayment cp ->
"Оплата картой " + cp.getCardNumber() +
" на сумму " + cp.getAmount();
case CashPayment cp ->
"Оплата наличными: " + cp.getAmount();
case TransferPayment tp ->
"Перевод на счёт " + tp.getAccountNumber() +
" на сумму " + tp.getAmount();
};
}
public static void main(String[] args) {
Payment card = new CardPayment("1234-5678-9012-3456", 1500);
Payment cash = new CashPayment(500);
Payment transfer = new TransferPayment("40702810100000001234", 3000);
System.out.println(process(card));
System.out.println(process(cash));
System.out.println(process(transfer));
}
}
Вывод программы:
Оплата картой 1234-5678-9012-3456 на сумму 1500.0
Оплата наличными: 500.0
Перевод на счёт 40702810100000001234 на сумму 3000.0
Обратите внимание: в switch нет default, и компилятор не ругается — он знает, что все типы обработаны!
Аналогия: Pattern matching с sealed classes — это как форма с выпадающим списком. Выборка уже знает все возможные варианты и не позволит выбрать несуществующий.
sealed vs final
Не путайте sealed и final:
| Характеристика | sealed | final |
|---|---|---|
| Наследование | Только разрешённые классы | Никто не может наследовать |
| permits | Обязателен (явно или в том же файле) | Не нужен |
| Иерархия | Может быть много уровней | Только один уровень |
| Когда использовать | Известные, но расширяемые наборы типов | Фиксированные типы |
Практический пример: форма
Давайте создадим систему форм с разными типами полей:
public sealed interface FormField
permits TextField, NumberField, CheckboxField {
String getLabel();
String getValueAsString();
}
public final class TextField implements FormField {
private final String label;
private final String value;
private final int maxLength;
public TextField(String label, String value, int maxLength) {
this.label = label;
this.value = value;
this.maxLength = maxLength;
}
@Override
public String getLabel() {
return label;
}
@Override
public String getValueAsString() {
return value;
}
public boolean isValid() {
return value != null && value.length() <= maxLength;
}
}
public final class NumberField implements FormField {
private final String label;
private final double value;
private final double min;
private final double max;
public NumberField(String label, double value, double min, double max) {
this.label = label;
this.value = value;
this.min = min;
this.max = max;
}
@Override
public String getLabel() {
return label;
}
@Override
public String getValueAsString() {
return String.valueOf(value);
}
public boolean isValid() {
return value >= min && value <= max;
}
}
public final class CheckboxField implements FormField {
private final String label;
private final boolean checked;
public CheckboxField(String label, boolean checked) {
this.label = label;
this.checked = checked;
}
@Override
public String getLabel() {
return label;
}
@Override
public String getValueAsString() {
return checked ? "Да" : "Нет";
}
public boolean isChecked() {
return checked;
}
}
public class FormProcessor {
public static String describe(FormField field) {
return switch (field) {
case TextField tf ->
"Текст: " + tf.getLabel() + " = \"" + tf.getValueAsString() + "\"";
case NumberField nf ->
"Число: " + nf.getLabel() + " = " + nf.getValueAsString();
case CheckboxField cb ->
"Галочка: " + cb.getLabel() + " = " + cb.getValueAsString();
};
}
public static void main(String[] args) {
FormField name = new TextField("Имя", "Иван", 50);
FormField age = new NumberField("Возраст", 25, 0, 150);
FormField agree = new CheckboxField("Согласен", true);
System.out.println(describe(name));
System.out.println(describe(age));
System.out.println(describe(agree));
}
}
Вывод программы:
Текст: Имя = "Иван"
Число: Возраст = 25.0
Галочка: Согласен = Да
Когда использовать sealed classes?
- Когда вы точно знаете все варианты типов
- Когда нужно гарантировать полноту обработки (pattern matching)
- Для моделирования состояний (светофор, заказ, задача)
- Для AST (abstract syntax tree) компиляторов
- Для обработки событий с фиксированным набором типов
non-sealed класс
Модификатор non-sealed открывает класс для наследования — любой класс может его наследовать:
public sealed interface Vehicle
permits Car, Truck, Motorcycle {
String getType();
int getWheels();
}
public final class Car implements Vehicle {
@Override
public String getType() { return "Легковой автомобиль"; }
@Override
public int getWheels() { return 4; }
}
public non-sealed class Truck implements Vehicle {
@Override
public String getType() { return "Грузовик"; }
@Override
public int getWheels() { return 6; }
}
public final class Motorcycle implements Vehicle {
@Override
public String getType() { return "Мотоцикл"; }
@Override
public int getWheels() { return 2; }
}
// Теперь кто угодно может наследовать Truck:
// public class FireTruck extends Truck { ... }
// public class SchoolBus extends Truck { ... }
Расширенный пример: вычисление выражений
Sealed classes идеально подходят для моделирования AST (дерева синтаксиса) компилятора или калькулятора:
public sealed interface Expression
permits NumberExpr, AdditionExpr, MultiplicationExpr {
double evaluate();
String toInfix();
}
public record NumberExpr(double value) implements Expression {
@Override
public double evaluate() {
return value;
}
@Override
public String toInfix() {
return String.valueOf(value);
}
}
public record AdditionExpr(Expression left, Expression right) implements Expression {
@Override
public double evaluate() {
return left.evaluate() + right.evaluate();
}
@Override
public String toInfix() {
return "( " + left.toInfix() + " + " + right.toInfix() + " )";
}
}
public record MultiplicationExpr(Expression left, Expression right) implements Expression {
@Override
public double evaluate() {
return left.evaluate() * right.evaluate();
}
@Override
public String toInfix() {
return "( " + left.toInfix() + " * " + right.toInfix() + " )";
}
}
public class ExpressionDemo {
public static void main(String[] args) {
Expression expr = new AdditionExpr(
new MultiplicationExpr(new NumberExpr(2), new NumberExpr(3)),
new NumberExpr(4)
);
System.out.println("Выражение: " + expr.toInfix());
System.out.println("Результат: " + expr.evaluate());
}
}
Вывод программы:
Выражение: ( ( 2.0 * 3.0 ) + 4.0 )
Результат: 10.0
Итоги урока
Итоги урока
- Sealed classes контролируют, кто может наследовать класс/реализовывать интерфейс
- Ключевое слово
permitsуказывает разрешённые подтипы - Подтипы должны быть:
final,sealedилиnon-sealed - Идеально сочетаются с pattern matching в switch
- Компилятор проверяет полноту обработки всех типов
- Применимы для: форм, AST, состояний, событий
- С Java 17+
Следующий урок: Внутренние классы →
Тест: Sealed classes
5 вопросов