☕ Вопросы интервью по Java

Вопросы 231-240 - Sealed-классы и pattern matching

Вопрос 231
Что такое sealed-классы и интерфейсы?

Ответ: Sealed (запечатанный) класс или интерфейс явно перечисляет, какие классы могут от него наследоваться. Появились в Java 17 (в 15 и 16 были в режиме preview). Это золотая середина между открытым наследованием и final.


public sealed interface Shape permits Circle, Square, Rectangle { }

public record Circle(double radius) implements Shape { }
public record Square(double side) implements Shape { }
public record Rectangle(double w, double h) implements Shape { }

// class Triangle implements Shape { }   // ОШИБКА: не входит в permits
                
Зачем:
  • Автор иерархии контролирует, кто может её расширять
  • Компилятор знает полный набор подтипов, поэтому switch можно проверять на полноту
  • Можно моделировать «один из нескольких вариантов» (алгебраические типы данных)
Вопрос 232
Какие правила действуют для sealed и его наследников?

Ответ:

  • Разрешённые наследники указываются в permits
  • Наследник обязан быть в том же модуле, а если модулей нет — в том же пакете
  • Наследник должен напрямую расширять sealed-тип
  • Каждый наследник обязан явно выбрать один из модификаторов: final, sealed или non-sealed
  • Record и enum неявно final, поэтому модификатор не нужен
  • Если все наследники объявлены в том же файле, permits можно опустить — компилятор выведет их сам

sealed interface Vehicle permits Car, Truck, Bike { }

final class Car implements Vehicle { }                   // дальше нельзя
sealed class Truck implements Vehicle permits Pickup { } // продолжает ограничение
final class Pickup extends Truck { }
non-sealed class Bike implements Vehicle { }             // снова открыт для наследования
                
Вопрос 233
Для чего нужны final, sealed и non-sealed у наследника?

Ответ:

Модификатор Что означает
final Иерархия здесь заканчивается, наследников нет
sealed Наследники есть, но только из своего списка permits
non-sealed Снимает ограничение: класс снова открыт для любого наследования
💡 Важно: non-sealed — осознанный выход из «замкнутой» иерархии. Компилятор по-прежнему будет считать набор прямых наследников sealed-родителя полным, но ниже этого класса подтипов может быть сколько угодно.
Вопрос 234
Чем sealed отличается от final, enum и абстрактного класса?

Ответ:

Конструкция Особенность
final класс Наследников нет вообще
Обычный (abstract) класс Наследовать может кто угодно, набор подтипов неизвестен
enum Фиксированный набор экземпляров одного типа, у всех одинаковая структура
sealed Фиксированный набор типов, и у каждого могут быть свои поля и состояние

Пример: результат операции — либо успех со значением, либо ошибка с сообщением. Это разные по структуре типы, поэтому здесь подходит sealed, а не enum.


sealed interface Result permits Ok, Fail { }
record Ok(String value) implements Result { }
record Fail(int code, String message) implements Result { }
                
Вопрос 235
Что такое pattern matching для instanceof?

Ответ: Начиная с Java 16, после проверки instanceof можно сразу объявить переменную нужного типа без явного приведения.


// раньше
if (obj instanceof String) {
    String s = (String) obj;
    System.out.println(s.length());
}

// сейчас
if (obj instanceof String s) {
    System.out.println(s.length());
}

// с условием
if (obj instanceof String s && s.length() > 5) {
    System.out.println(s.toUpperCase());
}

// отрицание: переменная доступна после if
if (!(obj instanceof String s)) {
    return;
}
System.out.println(s.length());
                
💡 Важно: область видимости переменной s определяется потоком выполнения (flow scoping): она доступна только там, где компилятор уверен, что проверка прошла успешно.
Вопрос 236
Что такое pattern matching в switch?

Ответ: В Java 21 switch может сопоставлять значение не только с константами, но и с типами (шаблонами), в том числе с дополнительным условием when.


static String describe(Object o) {
    return switch (o) {
        case null          -> "null";
        case Integer i when i > 100 -> "большое число " + i;
        case Integer i     -> "число " + i;
        case String s      -> "строка длиной " + s.length();
        case int[] arr     -> "массив из " + arr.length;
        default            -> "другое: " + o;
    };
}
                
Правила:
  • Ветки проверяются сверху вниз; более общий шаблон нельзя ставить выше более частного (ошибка dominated)
  • case null обрабатывает null явно; без него switch по-прежнему бросает NullPointerException
  • Switch должен быть полным (exhaustive): либо есть default, либо охвачены все варианты
Вопрос 237
Как sealed помогает switch обойтись без default?

Ответ: Если тип sealed, компилятор знает все варианты и проверяет, что switch покрывает каждый из них. Ветка default не нужна — и это хорошо: при добавлении нового наследника код перестанет компилироваться, пока вы не обработаете его.


sealed interface Shape permits Circle, Square, Rectangle { }
record Circle(double r) implements Shape { }
record Square(double side) implements Shape { }
record Rectangle(double w, double h) implements Shape { }

static double area(Shape s) {
    return switch (s) {
        case Circle c    -> Math.PI * c.r() * c.r();
        case Square sq   -> sq.side() * sq.side();
        case Rectangle r -> r.w() * r.h();
        // default не нужен
    };
}
                
💡 Важно: если позже добавить Triangle в permits, метод area выдаст ошибку компиляции: the switch expression does not cover all possible input values. С default эта ошибка была бы скрыта.
Вопрос 238
Как совместить sealed, record и record patterns?

Ответ: Связка «sealed-интерфейс + record-варианты» даёт алгебраические типы данных, а record patterns позволяют сразу доставать компоненты в switch (Java 21).


sealed interface Expr permits Num, Add, Mul { }
record Num(int value) implements Expr { }
record Add(Expr left, Expr right) implements Expr { }
record Mul(Expr left, Expr right) implements Expr { }

static int eval(Expr e) {
    return switch (e) {
        case Num(int v)          -> v;
        case Add(Expr l, Expr r) -> eval(l) + eval(r);
        case Mul(Expr l, Expr r) -> eval(l) * eval(r);
    };
}

eval(new Add(new Num(2), new Mul(new Num(3), new Num(4))));   // 14
                
💡 Совет: такой подход заменяет паттерн «Посетитель» (Visitor) для закрытых иерархий — код короче, а полнота проверяется компилятором.
Вопрос 239
Как sealed взаимодействует с рефлексией и сериализацией?

Ответ: В Class добавлены методы для работы с sealed-иерархиями (Java 17):


Shape.class.isSealed();                       // true

for (Class<?> c : Shape.class.getPermittedSubclasses()) {
    System.out.println(c.getSimpleName());
}
// Circle
// Square
// Rectangle
                
Особенности:
  • Ограничение permits проверяется при загрузке класса JVM, а не только компилятором, поэтому подменить иерархию через байткод не получится
  • Для класса, не являющегося sealed, getPermittedSubclasses() вернёт null
  • Анонимные классы не могут наследовать sealed-тип, а Proxy.newProxyInstance() не создаёт прокси для sealed-интерфейса
💡 Важно: sealed-тип нельзя подменить анонимным подклассом или динамическим прокси, например в тестах. Для таких случаев используйте конкретные подтипы.
Вопрос 240
Когда стоит использовать sealed, а когда нет?

Ответ:

Подходит, когда:
  • Набор вариантов известен и невелик: результаты, события, команды, узлы дерева, типы выражений
  • Нужна проверка полноты в switch
  • Вы пишете модель предметной области и хотите запретить внешние реализации
Не подходит, когда:
  • Иерархия должна расширяться сторонним кодом (плагины, SPI, публичные API библиотек)
  • Подтипы находятся в разных модулях или пакетах без модулей
  • Подходит простой enum — если у вариантов одинаковая структура
Краткая шпаргалка по версиям:
Возможность Финальная версия
switch-выражения Java 14
Pattern matching для instanceof Java 16
Records Java 16
Sealed-классы Java 17
Pattern matching для switch и record patterns Java 21