Какие требования к модификатору прямого наследника sealed класса предотвращают появление неучтённых ветвей ...

Какие требования к модификатору прямого наследника sealed-класса предотвращают появление неучтённых ветвей наследования?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Прямой наследник sealed-класса обязан быть объявлен как final, sealed или non-sealed. final полностью закрывает дальнейшее наследование, sealed продолжает контролируемую иерархию, а non-sealed намеренно открывает её для произвольных подклассов. Одного модификатора abstract недостаточно.

Исторический контекст

Обычное наследование в Java открыто: любой доступный не-final класс можно расширить, поэтому разработчик базового класса не может заранее знать полный набор реализаций. Sealed-классы, стандартизированные в Java 17, предназначены для явного описания закрытых иерархий, где набор прямых наследников контролируется автором базового типа.

Это полезно для моделирования конечного набора вариантов и для статического анализа, например проверки полноты обработки типов в switch.

Постановка проблемы

Если прямой наследник sealed-класса не ограничить, он сможет создать новую ветвь наследования вне объявленной модели. Тогда базовый класс формально задавал бы закрытую иерархию, но фактически не контролировал бы её продолжение.

Java предотвращает это на этапе компиляции: каждый прямой наследник должен быть указан среди разрешённых типов и явно определить дальнейшую политику наследования. Неверный модификатор или отсутствие типа в списке разрешённых наследников приводит к ошибке компиляции.

Подробное решение

У sealed-класса задаётся набор разрешённых прямых наследников через permits. Каждый такой наследник обязан выбрать один из трёх вариантов:

  • final — у типа не может быть подклассов;
  • sealed — тип остаётся закрытым и сам задаёт собственный список разрешённых наследников;
  • non-sealed — ограничения sealed-иерархии снимаются начиная с этого типа, поэтому его можно свободно расширять.
sealed class Command permits Create, Retry {} final class Create extends Command {} non-sealed class Retry extends Command {} class RetryLater extends Retry {} // class Delete extends Command {} // ошибка компиляции

RetryLater допустим, потому что Retry объявлен как non-sealed. Напрямую унаследовать Command от Delete нельзя: этот тип не указан в permits.

Прямой наследник также должен находиться в том же именованном модуле, что и sealed-класс, либо в том же пакете для неназванного модуля. Если все разрешённые наследники объявлены в той же единице компиляции, список permits в некоторых случаях может быть выведен компилятором.

Компромисс заключается в балансе контроля и расширяемости. sealed повышает предсказуемость и помогает анализу всех вариантов, но изменение списка разрешённых наследников становится частью контракта и может потребовать обновления зависимого кода.

Ситуация из практики

В библиотеке моделируются команды обработки заказов: создание, отмена и повторная попытка. Автор библиотеки хочет, чтобы внутренний код мог рассматривать эти варианты как полный набор.

Вариант с обычным базовым классом проще для расширения, но любой клиент сможет добавить новую команду, о которой библиотека не знает. Это удобно для плагинов, однако ухудшает контроль и полноту анализа.

Вариант с sealed позволяет явно перечислить поддерживаемые команды. Для редкого расширяемого направления можно объявить отдельный разрешённый подкласс как non-sealed; остальные ветви при этом останутся закрытыми.

Практический выбор — использовать sealed для стабильной предметной модели, а non-sealed только там, где расширение действительно является частью дизайна. Такой подход сохраняет проверяемость основной иерархии, не запрещая контролируемые точки расширения.

Что кандидаты часто упускают

  1. Достаточно ли объявить прямого наследника абстрактным?

Нет. abstract описывает возможность создавать подклассы и отсутствие обязанности создавать экземпляры, но не задаёт политику дальнейшего наследования. Абстрактный прямой наследник sealed-класса всё равно должен быть final, sealed или non-sealed.

Например, комбинация abstract sealed допустима, если тип продолжает закрытую иерархию и перечисляет разрешённых наследников. Комбинация abstract non-sealed тоже допустима, если после этого уровня наследование должно стать открытым.

  1. Может ли сторонняя библиотека добавить прямого наследника sealed-класса?

Как правило, нет. Прямой наследник должен быть явно указан в permits, а разрешённые типы должны находиться в том же именованном модуле либо в том же пакете для неназванного модуля.

Следовательно, sealed-иерархия не подходит как механизм, если независимые поставщики должны без согласования добавлять новые прямые реализации. Для такого сценария лучше обычный открытый базовый тип или интерфейс.

  1. Что произойдёт с обработкой вариантов после добавления нового разрешённого наследника?

При повторной компиляции кода, который исчерпывающе обрабатывает sealed-иерархию, компилятор может потребовать обработать новый вариант, например в switch без универсальной ветки. Это одно из преимуществ закрытой иерархии: изменения модели обнаруживаются статически.

Однако sealed-класс не обновляет автоматически уже скомпилированный клиентский код. Поэтому добавление разрешённого наследника — изменение контракта, которое требует проверки бинарной совместимости, повторной компиляции и тестирования зависимых компонентов.