![Запечатанные (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 {
}
public final class Class1 extends Superclass {
}
public sealed class Class2 extends Superclass permits Class4 {
}
public non-sealed class Class3 extends Superclass {
}
public final class Class4 extends Class2 {
}
public class OtherClass extends Superclass{
}
💡 Пример из практики.
В учебном примере «Умный дом» можно построить sealed-иерархию устройств:
package sealed;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
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();
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 interface Switchable permits AbstractDevice {
void turnOn();
void turnOff();
}
sealed abstract class AbstractDevice implements Switchable
permits AirConditioner, Lamp, AlarmSystem, SmartLamp {
private static final ConcurrentHashMap<Class<?>, AtomicInteger> counters = new ConcurrentHashMap<>();
private final int id;
protected AbstractDevice() {
this.id = counters.computeIfAbsent(getClass(), k -> new AtomicInteger()).incrementAndGet();
}
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 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 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 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 class FireAlarmSystem extends AlarmSystem {
@Override
public void turnOn() {
System.out.println(displayName() + " #" + getId() + " is monitoring heat and smoke...");
}
}
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);
}
}
final class RgbLamp extends SmartLamp {
public void setBrightness(int percent) {
System.out.println(displayName() + " #" + getId() + " brightness set to " + percent + "%.");
}
}
Здесь видно, что базовый класс 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;
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