1. Синтаксис sealed-классов: как это выглядит
Давайте начнём с классической проблемы ООП в Java: открытая иерархия наследования. В обычной Java любой желающий может унаследоваться от вашего класса, если только он не объявлен как final. Это удобно, но иногда приводит к неожиданностям — например, вы не можете заранее знать, какие именно классы будут наследниками вашего класса, а значит, не можете гарантировать, что обработали все варианты в switch или if-else.
В результате, когда вы пишете обработку объектов по типу (например, через pattern matching в switch), вы вынуждены добавлять ветку default «на всякий случай»: вдруг кто-то где-то создал новый подкласс?
Sealed-классы решают эту проблему: они позволяют явно ограничить список классов-наследников. Это делает иерархию закрытой и контролируемой, а ваш код — более предсказуемым и безопасным.
Основной синтаксис
Sealed-классы появились в Java 17. Они объявляются с помощью модификатора sealed, а список разрешённых наследников указывается через ключевое слово permits:
public sealed class Shape permits Circle, Rectangle, Square {
// Общее поведение для всех фигур
}
Здесь мы объявили класс Shape, и только классы Circle, Rectangle и Square могут быть его прямыми наследниками. Никто другой не сможет расширить Shape — компилятор не позволит.
Важно: Все классы, указанные в permits, должны быть объявлены в том же файле или быть видимыми для компилятора (обычно в том же пакете). Кстати, если все наследники объявлены в одном файле с sealed-классом, permits можно вообще опустить — компилятор сам всё поймёт.
Пример:
// Всё в одном файле - permits необязателен
public sealed class Shape {
}
final class Circle extends Shape {}
final class Rectangle extends Shape {}
Требования к наследникам
Каждый из наследников обязан явно определить свой статус:
- быть final (запрещает дальнейшее наследование),
- или быть sealed (и дальше ограничивать наследование),
- или быть non-sealed (разрешает наследование, снимает ограничения).
Пример:
public sealed class Shape permits Circle, Rectangle, Square {}
public final class Circle extends Shape {}
public sealed class Rectangle extends Shape permits FilledRectangle, EmptyRectangle {}
public non-sealed class Square extends Shape {}
- Circle — окончательный, дальше наследовать нельзя.
- Rectangle — сам sealed, разрешает только двум подклассам.
- Square — non-sealed, любой может расширить.
Минимальный пример
public sealed class Animal permits Dog, Cat {}
public final class Dog extends Animal {}
public final class Cat extends Animal {}
Попробуйте объявить новый класс public class Wolf extends Animal {} — получите ошибку компиляции:
class Wolf is not allowed to extend sealed class Animal
2. Применение sealed-классов: где и зачем использовать
Pattern matching и switch
Sealed-классы особенно хорошо сочетаются с pattern matching в switch. Если компилятор знает все возможные подклассы, он может убедиться, что вы обработали каждый вариант, и даже не потребует ветку default.
public sealed interface Result permits Success, Error {}
public final class Success implements Result {
public final String data;
public Success(String data) { this.data = data; }
}
public final class Error implements Result {
public final String message;
public Error(String message) { this.message = message; }
}
public class Main {
public static void main(String[] args) {
Result result = new Success("Ура!");
switch (result) {
case Success s -> System.out.println("Успех: " + s.data);
case Error e -> System.out.println("Ошибка: " + e.message);
}
}
}
Компилятор знает, что других вариантов у Result быть не может, и не требует default. Если вы забудете обработать один из вариантов, компилятор тут же вам напомнит.
Обратите внимание: начиная с Java 21, если в switch не все варианты обработаны, вы получите ошибку компиляции. В более ранних версиях (17–20) может потребоваться default, но IDE всё равно предупредит о неполном покрытии.
Безопасность и контроль
Sealed-классы позволяют разработчику полностью контролировать иерархию. Это особенно важно для доменных моделей, где набор вариантов должен быть фиксирован (например, состояние заказа: New, Paid, Cancelled).
Упрощение поддержки и развития кода
Когда вы знаете все варианты наследников, проще добавлять новые фичи, поддерживать код и проводить рефакторинг. IDE тоже будет «в курсе» всех вариантов и сможет помогать с автодополнением и анализом.
3. Практические примеры использования sealed-классов
Пример 1: Геометрические фигуры
public sealed interface Shape permits Circle, Rectangle, Square {}
public final class Circle implements Shape {
public final double radius;
public Circle(double radius) { this.radius = radius; }
}
public final class Rectangle implements Shape {
public final double width, height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
}
public final class Square implements Shape {
public final double side;
public Square(double side) { this.side = side; }
}
Теперь можно безопасно использовать switch с pattern matching:
Shape shape = new Circle(5);
switch (shape) {
case Circle c -> System.out.println("Круг радиусом " + c.radius);
case Rectangle r -> System.out.println("Прямоугольник " + r.width + "x" + r.height);
case Square s -> System.out.println("Квадрат со стороной " + s.side);
}
Пример 2: Финансовые транзакции
public sealed interface Transaction permits Deposit, Withdraw, Transfer {}
public final class Deposit implements Transaction {
public final double amount;
public Deposit(double amount) { this.amount = amount; }
}
public final class Withdraw implements Transaction {
public final double amount;
public Withdraw(double amount) { this.amount = amount; }
}
public final class Transfer implements Transaction {
public final double amount;
public final String toAccount;
public Transfer(double amount, String toAccount) {
this.amount = amount;
this.toAccount = toAccount;
}
}
Теперь в обработчике можно быть уверенным, что вы не забыли ни один тип транзакции:
Transaction tx = new Transfer(100, "ACC123");
switch (tx) {
case Deposit d -> System.out.println("Депозит: " + d.amount);
case Withdraw w -> System.out.println("Снятие: " + w.amount);
case Transfer t -> System.out.println("Перевод: " + t.amount + " на " + t.toAccount);
}
Пример 3: Ограниченная иерархия с non-sealed
public sealed class Notification permits EmailNotification, SmsNotification, PushNotification {}
public final class EmailNotification extends Notification {}
public non-sealed class SmsNotification extends Notification {}
public final class PushNotification extends Notification {}
// Теперь кто угодно может наследовать SmsNotification
public class ViberNotification extends SmsNotification {}
4. Особенности, ограничения и нюансы sealed-классов
Требования к модификаторам
- Sealed-класс обязан явно указать всех наследников через permits.
- Все наследники должны быть либо final, либо sealed, либо non-sealed.
- Наследники должны быть объявлены либо в том же файле, либо быть видимыми для компилятора.
Абстрактные sealed-классы
Sealed-класс может быть и абстрактным, и interface, и обычным классом. Например:
public sealed abstract class Expr permits Const, Add, Mul {}
Совместимость с другими модификаторами
- Нельзя объявить sealed-класс как final или non-sealed.
- interface тоже может быть sealed (и это очень удобно!).
Использование с record-классами
Record-классы могут быть наследниками sealed-класса, если они объявлены как final (по умолчанию record всегда final):
public sealed interface Expr permits Const, Add, Mul {}
public record Const(int value) implements Expr {}
public record Add(Expr left, Expr right) implements Expr {}
public record Mul(Expr left, Expr right) implements Expr {}
5. Полезные нюансы
Sealed-классы и pattern matching: как это связано
Главная фишка sealed-классов — исчерпывающий pattern matching. Звучит страшно, работает просто. Компилятор знает все возможные варианты, и вы можете смело писать switch без ветки default:
Expr expr = ...;
switch (expr) {
case Const c -> ...
case Add a -> ...
case Mul m -> ...
}
Если вы вдруг не обработаете какой-то вариант, компилятор не даст собрать проект — это очень удобно и безопасно.
Применение в реальных задачах
Где Sealed-классы реально полезны? Везде, где у вас есть фиксированный набор вариантов:
- Результат операции: Success, Error (как в примере выше).
- Состояние заказа: New, Paid, Cancelled.
- Абстрактные синтаксические деревья (AST) для парсеров.
- Ответы API: Ok, NotFound, Error.
- События в системе: UserLoggedIn, UserLoggedOut, UserRegistered.
6. Типичные ошибки при работе с sealed-классами
Ошибка №1: забыли указать всех наследников в permits.
Если вы не перечислили всех нужных наследников через permits, компилятор тут же пожалуется. Например, если вы написали permits Circle, Rectangle, но забыли Square, а такой класс есть — получите ошибку.
Ошибка №2: наследник не final, sealed или non-sealed.
Если наследник не объявлен с нужным модификатором, компилятор выдаст ошибку: «Class must be either final, sealed or non-sealed».
Ошибка №3: наследник объявлен в другом файле и не виден компилятору.
Все перечисленные в permits классы должны быть доступны компилятору — либо в том же файле, либо в том же пакете.
Ошибка №4: не обработали все варианты в switch.
Если вы используете sealed-класс в switch с pattern matching и забыли обработать какой-то вариант, компилятор не даст собрать проект. Это хорошо — вы не пропустите ни один случай.
Ошибка №5: попытка унаследоваться от sealed-класса не из permits.
Если вы попробуете создать класс-наследник, не указанный в permits, получите ошибку: «is not allowed to extend sealed class».
Ошибка №6: попытка использовать sealed-классы на старых версиях JDK.
Sealed-классы появились только в Java 17. Если вы попробуете использовать их в более старой версии, получите ошибку компиляции или вообще не сможете собрать проект.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ