JavaRush /Курсы /JAVA 25 SELF /Sealed classes: синтаксис и применение

Sealed classes: синтаксис и применение

JAVA 25 SELF
65 уровень , 0 лекция
Открыта

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, разрешает только двум подклассам.
  • Squarenon-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. Если вы попробуете использовать их в более старой версии, получите ошибку компиляции или вообще не сможете собрать проект.

1
Задача
JAVA 25 SELF, 65 уровень, 0 лекция
Недоступна
Нарушение ограничений в системе транспорта 🚫
Нарушение ограничений в системе транспорта 🚫
1
Задача
JAVA 25 SELF, 65 уровень, 0 лекция
Недоступна
Гибкая система оповещений 📱
Гибкая система оповещений 📱
Комментарии (1)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Anton Pohodin Уровень 26
29 ноября 2025
Надо же, первый!) Level 65!