Sealed классы (Java 17)

Sealed (запечатанный) класс или интерфейс — это тип, который ограничивает, какие другие классы или интерфейсы могут его наследовать или реализовывать. Фича появилась как preview в Java 15 (JEP 360) и стала стабильной в Java 17 (JEP 409).

До Java 17 у разработчика было только два крайних варианта: либо открытое наследование (public class), либо полный запрет (final). Sealed-классы дают третий вариант: «ограниченный набор подтипов, известный заранее». Это особенно полезно вместе с pattern matching: компилятор может проверить, что вы обработали все варианты в switch.

Минимальная версия: Java 17.

Синтаксис

public sealed class Parent permits Child1, Child2, Child3 { }

// Каждый наследник обязан быть final, sealed или non-sealed:
public final class Child1 extends Parent { }
public sealed class Child2 extends Parent permits SubChild { }
public non-sealed class Child3 extends Parent { }

Зачем нужно

  • Моделирование закрытых иерархий: «либо A, либо B, либо C, других не бывает».

  • Безопасный pattern matching: компилятор знает все варианты и требует обработать каждый.

  • Контроль API: автор библиотеки гарантирует, что внешние пользователи не подменят реализацию.

  • Альтернатива enum, когда у вариантов разный набор полей.

Пример 1. Базовая иерархия фигур

public class SealedShapes {
    public sealed interface Shape permits Circle, Square, Triangle { }

    public record Circle(double radius) implements Shape { }
    public record Square(double side) implements Shape { }
    public record Triangle(double base, double height) implements Shape { }

    public static double area(Shape s) {
        return switch (s) {
            case Circle c -> Math.PI * c.radius() * c.radius();
            case Square sq -> sq.side() * sq.side();
            case Triangle t -> 0.5 * t.base() * t.height();
        };
    }

    public static void main(String[] args) {
        System.out.printf("Круг:        %.2f%n", area(new Circle(5)));
        System.out.printf("Квадрат:     %.2f%n", area(new Square(4)));
        System.out.printf("Треугольник: %.2f%n", area(new Triangle(3, 6)));
    }
}

Output:

Круг:        78,54
Квадрат:     16,00
Треугольник: 9,00

Пример 2. sealed + final

public class SealedFinal {
    public sealed class Vehicle permits Car, Truck { }
    public final class Car extends Vehicle { }
    public final class Truck extends Vehicle { }

    public static void main(String[] args) {
        Vehicle v1 = new Car();
        Vehicle v2 = new Truck();
        System.out.println("v1: " + v1.getClass().getSimpleName());
        System.out.println("v2: " + v2.getClass().getSimpleName());
    }
}

Output:

v1: Car
v2: Truck

Пример 3. non-sealed — открыть ветку обратно

public class SealedNonSealed {
    public sealed class Component permits Sensor, Actuator { }

    // Sensor закрыт — ничего наследовать нельзя
    public final class Sensor extends Component { }

    // Actuator открыт для дальнейшего наследования
    public non-sealed class Actuator extends Component { }

    // Можно свободно наследовать от Actuator
    public class Servo extends Actuator { }
    public class Motor extends Actuator { }

    public static void main(String[] args) {
        Component a = new Servo();
        Component b = new Motor();
        System.out.println(a.getClass().getSimpleName() + " — это Actuator");
        System.out.println(b.getClass().getSimpleName() + " — это Actuator");
    }
}

Output:

Servo — это Actuator
Motor — это Actuator

Пример 4. Вложенная sealed-иерархия

public class SealedNested {
    public sealed interface Command permits Move, Stop, Turn { }
    public sealed interface Move extends Command permits Forward, Backward { }

    public record Forward(int speed) implements Move { }
    public record Backward(int speed) implements Move { }
    public record Stop() implements Command { }
    public record Turn(int angle) implements Command { }

    public static String describe(Command c) {
        return switch (c) {
            case Forward f  -> "Вперёд на скорости " + f.speed();
            case Backward b -> "Назад на скорости " + b.speed();
            case Stop s     -> "Стоп";
            case Turn t     -> "Поворот на " + t.angle() + "°";
        };
    }

    public static void main(String[] args) {
        System.out.println(describe(new Forward(150)));
        System.out.println(describe(new Backward(100)));
        System.out.println(describe(new Stop()));
        System.out.println(describe(new Turn(90)));
    }
}

Output:

Вперёд на скорости 150
Назад на скорости 100
Стоп
Поворот на 90°

Пример 5. Exhaustive switch — компилятор требует все варианты

public class SealedExhaustive {
    public sealed interface Result permits Ok, Err { }
    public record Ok(String value) implements Result { }
    public record Err(String message) implements Result { }

    public static String handle(Result r) {
        // default не нужен — компилятор знает, что Result либо Ok, либо Err
        return switch (r) {
            case Ok ok   -> "OK: " + ok.value();
            case Err err -> "ERR: " + err.message();
        };
    }

    public static void main(String[] args) {
        System.out.println(handle(new Ok("ультразвук = 12 см")));
        System.out.println(handle(new Err("датчик не отвечает")));
    }
}

Output:

OK: ультразвук = 12 см
ERR: датчик не отвечает

Пример 6. Без permits в одном файле

// Если все подтипы объявлены в том же исходном файле,
// permits можно опустить — компилятор выведет список сам.
public class SealedImplicit {
    public sealed interface Signal { }
    public record High() implements Signal { }
    public record Low() implements Signal { }

    public static void main(String[] args) {
        Signal s = new High();
        System.out.println("Сигнал: " + s.getClass().getSimpleName());
    }
}

Output:

Сигнал: High

Подводные камни

Предупреждение

  • Каждый прямой наследник sealed-класса обязан быть либо final, либо sealed, либо non-sealed. Просто class не подойдёт.

  • Все наследники должны находиться в том же модуле (или в том же пакете для unnamed module).

  • permits можно опустить только если все наследники объявлены в одном файле с родителем.

  • non-sealed фактически «прокалывает дыру» в иерархии — используйте осознанно.

  • Records неявно final и идеально подходят как «листья» sealed-иерархии.

Совет

Sealed-классы + records + pattern matching образуют так называемые «алгебраические типы данных» (ADT) — мощный инструмент для моделирования предметной области.

См. также

Примечание

Материал основан на официальной документации Oracle Java SE (docs.oracle.com/en/java/javase) и спецификации JEP 409 (openjdk.org/jeps/409), распространяемой под лицензией Oracle Free Documentation License. Тексты и примеры написаны заново для AlashEd Wiki.