Какие требования к модификатору прямого наследника sealed-класса предотвращают появление неучтённых ветвей наследования?
Прямой наследник 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-иерархии снимаются начиная с этого типа, поэтому его можно свободно расширять.RetryLater допустим, потому что Retry объявлен как non-sealed. Напрямую унаследовать Command от Delete нельзя: этот тип не указан в permits.
Прямой наследник также должен находиться в том же именованном модуле, что и sealed-класс, либо в том же пакете для неназванного модуля. Если все разрешённые наследники объявлены в той же единице компиляции, список permits в некоторых случаях может быть выведен компилятором.
Компромисс заключается в балансе контроля и расширяемости. sealed повышает предсказуемость и помогает анализу всех вариантов, но изменение списка разрешённых наследников становится частью контракта и может потребовать обновления зависимого кода.
В библиотеке моделируются команды обработки заказов: создание, отмена и повторная попытка. Автор библиотеки хочет, чтобы внутренний код мог рассматривать эти варианты как полный набор.
Вариант с обычным базовым классом проще для расширения, но любой клиент сможет добавить новую команду, о которой библиотека не знает. Это удобно для плагинов, однако ухудшает контроль и полноту анализа.
Вариант с sealed позволяет явно перечислить поддерживаемые команды. Для редкого расширяемого направления можно объявить отдельный разрешённый подкласс как non-sealed; остальные ветви при этом останутся закрытыми.
Практический выбор — использовать sealed для стабильной предметной модели, а non-sealed только там, где расширение действительно является частью дизайна. Такой подход сохраняет проверяемость основной иерархии, не запрещая контролируемые точки расширения.
Нет. abstract описывает возможность создавать подклассы и отсутствие обязанности создавать экземпляры, но не задаёт политику дальнейшего наследования. Абстрактный прямой наследник sealed-класса всё равно должен быть final, sealed или non-sealed.
Например, комбинация abstract sealed допустима, если тип продолжает закрытую иерархию и перечисляет разрешённых наследников. Комбинация abstract non-sealed тоже допустима, если после этого уровня наследование должно стать открытым.
Как правило, нет. Прямой наследник должен быть явно указан в permits, а разрешённые типы должны находиться в том же именованном модуле либо в том же пакете для неназванного модуля.
Следовательно, sealed-иерархия не подходит как механизм, если независимые поставщики должны без согласования добавлять новые прямые реализации. Для такого сценария лучше обычный открытый базовый тип или интерфейс.
При повторной компиляции кода, который исчерпывающе обрабатывает sealed-иерархию, компилятор может потребовать обработать новый вариант, например в switch без универсальной ветки. Это одно из преимуществ закрытой иерархии: изменения модели обнаруживаются статически.
Однако sealed-класс не обновляет автоматически уже скомпилированный клиентский код. Поэтому добавление разрешённого наследника — изменение контракта, которое требует проверки бинарной совместимости, повторной компиляции и тестирования зависимых компонентов.