![Запечатанные (sealed) классы и интерфейсы в Java. - 1]()
Приветствую, коллеги! Java — язык, который не останавливается в развитии. С каждой новой версией он получает свежие возможности, делающие наш код более гибким и надёжным. Сегодня мы поговорим об одной из таких фич — sealed‑классах (запечатанных классах).
🔒 Запечатанные (sealed) классы и интерфейсы в Java.
Запечатанные (sealed) классы и интерфейсы — это новая возможность языка Java, которая появилась в релизе Java 15 как preview-фича, а окончательно стабилизировалась в Java 17.
Идея заключается в том, чтобы ограничить круг наследников для определённого класса или интерфейса. Это даёт разработчику контроль над иерархией и позволяет компилятору проверять исчерпывающие конструкции, такие как switch или pattern matching.
Официальное предложение описано в
JEP 360: Sealed Classes.
В Java 17 эта возможность стала стандартной частью языка.
❓ Зачем нужны sealed-классы?
● Контроль наследования — разработчик явно указывает, какие классы могут наследовать базовый sealed-тип.
● Повышение безопасности — ограничение круга наследников помогает избежать ошибок и упрощает анализ кода.
● Исчерпывающие проверки — компилятор знает все возможные варианты sealed-иерархии и может требовать полного покрытия в
switch или pattern matching.
● Удобство моделирования — sealed-иерархии хорошо подходят для моделирования закрытых наборов сущностей, например, типов событий, состояний, команд.
📝 Синтаксис.
Чтобы объявить sealed-тип, используется модификатор
sealed.
После этого необходимо указать список разрешённых наследников через ключевое слово
permits.
В отличие от обычного наследования, когда мы можем расширять суперкласс любым количеством классов, класс помеченный ключевым словом
sealed позволяет нам четко контролировать с помощью другого ключевого слова
permits список тех классов которые мы хотим разрешить унаследовать от данного суперкласса, тем самым ещё больше повышая инкапсуляцию нашего кода.
Наследники sealed-типа должны быть объявлены одним из трёх способов:
1.
final — окончательный класс, который нельзя расширять дальше.
2.
sealed — сам запечатанный класс, который продолжает ограничивать наследование.
3.
non-sealed — класс, который снимает ограничения и позволяет наследоваться свободно.
Всё вышесказанное справедливо и для интерфейсов и для абстрактных классов также.
Таким образом, у разработчика есть полный контроль: можно закрыть иерархию, можно продолжить ограничивать, а можно разрешить свободное расширение.
Вот как это выглядит в коде:
public sealed class Superclass permits Class1, Class2, Class3 {
// Это наш суперкласс и он позволяет расширять себя только классам Class1, Class2, Class3
}
public final class Class1 extends Superclass {
// Этот класс является допустимым для расширения sealed-суперкласса и помечен как финальный (нет наследников)
}
public sealed class Class2 extends Superclass permits Class4 {
// Этот класс расширяет sealed-суперкласс и сам является sealed-классом
}
public non-sealed class Class3 extends Superclass {
// А этот класс расширяет sealed-суперкласс, а ключевое слово non-sealed превращает его в обычный класс (можно наследовать без ограничений)
}
public final class Class4 extends Class2 {
}
public class OtherClass extends Superclass{
// Тут будет ошибка, т.к. OtherClass не входит в permits суперкласса Superclass
}
💡 Пример из практики.
В учебном примере «Умный дом» можно построить sealed-иерархию устройств:
/*
* Пример демонстрации sealed-иерархии в Java.
* Sealed-классы и интерфейсы появились как preview в Java 15 (JEP 360),
* а стали стандартом в Java 17.
*
* Идея: ограничить круг наследников, чтобы компилятор знал все варианты.
*/
package sealed;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Главный класс-демо.
* Здесь мы создаём список устройств и демонстрируем работу sealed-иерархии.
*/
public class SealedClassesDemo {
public static void main(String[] args) {
List<Switchable> devices = List.of(
new AirConditioner(),
new Lamp(),
new AlarmSystem(),
new FireAlarmSystem(),
new SmartLamp(),
new RgbLamp()
);
// Перебираем устройства и выполняем действия
for (Switchable device : devices) {
device.turnOn();
// Pattern matching for instanceof (Java 16+)
// Позволяет сразу "распаковать" объект в переменную нужного типа
if (device instanceof AirConditioner ac) {
ac.setTemperature(24);
} else if (device instanceof Lamp lamp) {
lamp.dim(70);
} else if (device instanceof FireAlarmSystem fire) {
fire.triggerAlarm();
} else if (device instanceof RgbLamp rgb) {
rgb.changeColor("Red");
rgb.setBrightness(80);
} else if (device instanceof SmartLamp smart) {
smart.changeColor("Blue");
smart.runScenario("Evening Relax");
} else if (device instanceof AlarmSystem alarm) {
alarm.triggerAlarm();
}
device.turnOff();
System.out.println();
}
}
}
/**
* Sealed интерфейс.
* Ограничивает круг реализаций: только AbstractDevice.
*/
sealed interface Switchable permits AbstractDevice {
void turnOn();
void turnOff();
}
/**
* Sealed абстрактный базовый класс.
* Разрешает наследование только указанным классам (AirConditioner, Lamp, AlarmSystem, SmartLamp).
* Здесь реализован общий функционал: генерация ID и красивое имя.
*/
sealed abstract class AbstractDevice implements Switchable
permits AirConditioner, Lamp, AlarmSystem, SmartLamp {
// Счётчики для каждого класса устройств
private static final ConcurrentHashMap<Class<?>, AtomicInteger> counters = new ConcurrentHashMap<>();
private final int id;
// Конструктор: каждому устройству присваивается уникальный ID
protected AbstractDevice() {
this.id = counters.computeIfAbsent(getClass(), k -> new AtomicInteger()).incrementAndGet();
}
// Метод для красивого отображения имени класса (разбивает CamelCase на слова)
protected String displayName() {
String className = getClass().getSimpleName();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < className.length(); i++) {
char ch = className.charAt(i);
if (Character.isUpperCase(ch) && i > 0) sb.append(' ');
sb.append(ch);
}
return sb.toString();
}
protected int getId() {
return id;
}
}
/**
* Кондиционер.
* final — значит, нельзя наследовать дальше.
*/
final class AirConditioner extends AbstractDevice {
@Override
public void turnOn() {
System.out.println(displayName() + " #" + getId() + " is turned ON.");
}
@Override
public void turnOff() {
System.out.println(displayName() + " #" + getId() + " is turned OFF.");
}
public void setTemperature(int degrees) {
System.out.println(displayName() + " #" + getId() + " set to " + degrees + "°C.");
}
}
/**
* Лампа.
* final — тоже закрытый класс.
*/
final class Lamp extends AbstractDevice {
@Override
public void turnOn() {
System.out.println(displayName() + " #" + getId() + " is turned ON.");
}
@Override
public void turnOff() {
System.out.println(displayName() + " #" + getId() + " is turned OFF.");
}
public void dim(int percent) {
System.out.println(displayName() + " #" + getId() + " dimmed to " + percent + "%.");
}
}
/**
* Система сигнализации.
* sealed — значит, можно наследовать, но только тем классам, что указаны в permits.
* Здесь разрешён только FireAlarmSystem.
*/
sealed class AlarmSystem extends AbstractDevice permits FireAlarmSystem {
@Override
public void turnOn() {
System.out.println(displayName() + " #" + getId() + " is turned ON.");
}
@Override
public void turnOff() {
System.out.println(displayName() + " #" + getId() + " is turned OFF.");
}
public void triggerAlarm() {
System.out.println(displayName() + " #" + getId() + " ALARM TRIGGERED!");
}
}
/**
* Пожарная сигнализация.
* final — закрытый класс, наследует AlarmSystem.
*/
final class FireAlarmSystem extends AlarmSystem {
@Override
public void turnOn() {
System.out.println(displayName() + " #" + getId() + " is monitoring heat and smoke...");
}
}
/**
* Смарт-лампа.
* non-sealed — снимает ограничения, можно наследовать свободно.
*/
non-sealed class SmartLamp extends AbstractDevice {
@Override
public void turnOn() {
System.out.println(displayName() + " #" + getId() + " is turned ON with smart features.");
}
@Override
public void turnOff() {
System.out.println(displayName() + " #" + getId() + " is turned OFF.");
}
public void changeColor(String color) {
System.out.println(displayName() + " #" + getId() + " changed color to " + color + ".");
}
public void runScenario(String scenario) {
System.out.println(displayName() + " #" + getId() + " running scenario: " + scenario);
}
}
/**
* RGB-лампа.
* final — закрытый класс, наследует SmartLamp.
*/
final class RgbLamp extends SmartLamp {
public void setBrightness(int percent) {
System.out.println(displayName() + " #" + getId() + " brightness set to " + percent + "%.");
}
}
// final class FloodControlAlarmSystem extends AlarmSystem { }
// ↑ Если раскомментировать — будет ошибка компиляции,
// потому что AlarmSystem разрешает наследование только FireAlarmSystem.
Здесь видно, что базовый класс AbstractDevice разрешает только четыре наследника. AlarmSystem сам является
sealed и разрешает только FireAlarmSystem. SmartLamp объявлен как
non-sealed, поэтому его можно расширять свободно, например, создавая RgbLamp.
🔒📑 sealed и record.
Комбинация
sealed и
record особенно удобна для моделирования закрытых наборов данных. Record-классы автоматически
final, а
sealed гарантирует, что список вариантов фиксирован. Это позволяет строить исчерпывающие проверки и упрощает работу с данными.
Пример:
sealed interface Device permits AirConditioner, Lamp {
}
record AirConditioner(int id) implements Device {
}
record Lamp(int id) implements Device {
}
Здесь интерфейс Device может иметь только два варианта: AirConditioner и Lamp. Компилятор знает все варианты и может проверять
switch на полноту.
🔍 Sealed‑классы и Reflection.
Java Reflection API позволяет исследовать структуру классов во время выполнения. Для sealed‑классов добавили специальные методы, чтобы можно было узнать список разрешённых наследников.
●
isSealed() — возвращает , если класс или интерфейс объявлен как
sealed.
●
getPermittedSubclasses() — возвращает массив (Java 17+) с информацией о разрешённых наследниках.
Пример кода:
public class ReflectionDemo {
public static void main(String[] args) {
Class<?> clazz = AbstractDevice.class;
// Проверяем, является ли класс sealed
System.out.println("Is sealed: " + clazz.isSealed());
// Получаем список разрешённых наследников
Class<?>[] permitted = clazz.getPermittedSubclasses();
System.out.println("Permitted subclasses of " + clazz.getSimpleName() + ":");
for (Class<?> desc : permitted) {
System.out.println(" - " + desc.getSimpleName());
}
}
}
Вывод программы:
Is sealed: true
Permitted subclasses of AbstractDevice:
- AirConditioner
- Lamp
- AlarmSystem
- SmartLamp
❓ Когда использовать sealed-классы?
● Когда нужно ограничить иерархию и предотвратить появление неожиданных наследников.
● Когда важно, чтобы компилятор знал все варианты и мог проверять исчерпывающие конструкции.
● Когда моделируется закрытый набор сущностей: состояния, события, команды, устройства.
● Когда хочется повысить читаемость и предсказуемость архитектуры.
📌 Вывод.
Запечатанные классы и интерфейсы — это мощный инструмент языка Java, который позволяет контролировать наследование и строить более надёжные архитектуры. В сочетании с
record и
pattern matching sealed‑иерархии дают разработчику удобный способ моделировать закрытые наборы сущностей и обеспечивать безопасность и предсказуемость кода. Эта возможность, ставшая стандартной частью языка начиная с Java 17, открывает новые горизонты для проектирования устойчивых и понятных систем.
Author: Denis Odesskiy (MyFreeIT)
Date of publication: 28 November 2025
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ