Запечатанные (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