Sealed (закрытые или запечатанные) классы были представлены в Java 15 в качестве способа ограничить иерархию наследования класса или интерфейса.

Sealed класс или интерфейс ограничивает набор классов, которые могут наследовать или реализовывать его, что может помочь предотвратить потенциальные ошибки и сделать код более удобным в сопровождении.

Предположим, вы создаете приложение для онлайн магазина, поддерживающее несколько разных способов оплаты, а именно кредитные карты, PayPal и биткоины. Вы можете определить sealed класс PaymentMethod, который имеет список допустимых подклассов (после ключевого слова permits) под каждый метод оплаты:

public sealed class PaymentMethod permits CreditCard, PayPal, Bitcoin {
   // Члены класса
}

В этом примере PaymentMethod — это sealed класс, который позволяет CreditCard, PayPal и Bitcoin расширять его. Sealed класс может разрешить любому количеству классов расширить его, указав их в списке, разделенном запятыми, после ключевого слова permits.

И это только один пример того, как использование sealed класса может облегчить нашу жизнь.

Итак, давайте разбираться!

Создание иерархии закрытого типа

Sealed классы могут создавать иерархию закрытого типа. Речь идет об ограниченном наборе классов, которые не могут быть расширены или реализованы вне определенного пакета.

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

package ca.bazlur
public sealed class Animal permits Cat, Dog {
   // Определение класса
}
public final class Cat extends Animal {
   // Определение класса
}
public final class Dog extends Animal {
   // Определение класса
}

В этом примере Animal является sealed классом, который позволяет расширять его только классам Cat и Dog.

Любая другая попытка расширения Animal приведет к ошибке компиляции.

Создание ограниченного набора реализаций

Sealed классы также могут создавать ограниченный набор реализаций для определенного интерфейса или абстрактного класса. Это нужно, чтобы владельцы интерфейса или абстрактного класса могли контролировать и изменять набор реализаций.

public sealed interface Shape permits Circle, Square {
   double getArea();
}
public final class Circle implements Shape {
   // Определение класса
}
public final class Square implements Shape {
   // Определение класса
}

В этом примере Shape — это sealed интерфейс, который позволяет реализовать его только классам Circle и Square.

Это гарантирует, что создать любую другую реализацию Shape будет невозможно.

Улучшение паттерн матчинга в switch-конструкциях

Sealed классы также можно использовать для улучшения паттерн матчинга в операторах ветвления.

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

public sealed abstract class PaymentMethod permits CreditCard, DebitCard, PayPal {
   // Определение класса
}
public class PaymentProcessor {
   public void processPayment(PaymentMethod paymentMethod, double amount) {
       switch (paymentMethod) {
           case CreditCard cc -> {
               // Обработка платежа кредитной картой
           }
           case DebitCard dc -> {
               // Обработка платежа дебетовой картой
           }
           case PayPal pp -> {
               // Обработка платежа по PayPal
           }
         
       }
   }
}

В этом примере PaymentMethod является sealed классом, который могут расширять классы CreditCard, DebitCard и PayPal.

Метод processPayment в классе PaymentProcessor использует оператор ветвления switch с паттерн матчингом для обработки различных способов оплаты.

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

Реализация конечного автомата

Sealed классы можно использовать для реализации конечного автомата (или стейт машины) — вычислительной модели, которая определяет поведение системы в ответ на ряд входных данных. В конечном автомате каждое состояние представлено sealed классом, а переход между состояниями реализован с помощью методов, возвращающих новое состояние.

public sealed class State permits IdleState, ActiveState, ErrorState {
   public State transition(Input input) {
       // Логика перехода
   }
}
public final class IdleState extends State {
   // Определение класса
}
public final class ActiveState extends State {
   // Определение класса
}
public final class ErrorState extends State {
   // Определение класса
}

В этом примере State является sealed классом, который допускает подклассы IdleState, ActiveState и ErrorState.

Метод transition отвечает за переход между состояниями на основе введенного input.

Использование sealed классов гарантирует, что конечный автомат структурирован должным образом и может быть расширен только ограниченным набором классов.

Создание ограниченного набора исключений

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

public sealed class DatabaseException extends Exception permits ConnectionException, QueryException {
   // Определение класса
}
public final class ConnectionException extends DatabaseException {
   // Определение класса
}
public final class QueryException extends DatabaseException {
   // Определение класса
}

В этом примере DatabaseException является sealed классом, который допускает подклассы ConnectionException и QueryException.

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

Управление доступом к конструкторам

Sealed классы также могут управлять доступом к конструкторам, что позволяет реализовать определенный набор инвариантов для класса.

public sealed class Person {
   private final String name;
   private final int age;
   private Person(String name, int age) {
       this.name = name;
       this.age = age;
   }
   public static final class Child extends Person {
       public Child(String name, int age) {
           super(name, age);
           if (age >= 18) {
               throw new IllegalArgumentException("Children must be under 18 years old.");
           }
       }
   }
   public static final class Adult extends Person {
       public Adult(String name, int age) {
           super(name, age);
           if (age < 18) {
               throw new IllegalArgumentException("Adults must be 18 years old or older.");
           }
       }
   }
}

В этом примере Person — это sealed класс с двумя подклассами: Child и Adult.

Конструкторы для классов Child и Adult указаны как public, а конструктор для Person указан как private, в результате чего создание инстансов Person возможно только через его подклассы.

Это позволяет Person обеспечить соблюдение инварианта о том, что дети должны быть моложе 18 лет, а взрослым должно быть 18 лет или больше.

Повышение безопасности кода

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

public sealed class SecureCode permits TrustedCode {
   // Определение класса
}
// Надежный код
public final class TrustedCode extends SecureCode {
   // Определение класса
}
// Ненадежный код
public final class UntrustedCode extends SecureCode {
   // Определение класса
}

В этом примере SecureCode является sealed классом, который допускает только подкласс TrustedCode.

Это помогает нам гарантировать, что только доверенный код может получить доступ к конфиденциальным частям кодовой базы.

Полиморфизм с исчерпывающим паттерн матчингом

Sealed классы также можно использовать для реализации полиморфизма с исчерпывающим паттерн матчингом.

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

public sealed class Shape permits Circle, Square {
   // Определение класса
}
public final class Circle extends Shape {
   // Определение класса
}
public final class Square extends Shape {
   // Определение класса
}
public void drawShape(Shape shape) {
   switch (shape) {
       case Circle c -> c.drawCircle();
       case Square s -> s.drawSquare();
   }
}

В этом примере Shape — это sealed класс, который позволяет классам Circle и Square расширять его.

Метод drawShape использует паттерн матчинг для отрисовки фигуры, гарантируя, что все возможные подтипы Shape охватываются оператором switch.

Повышение читабельности кода

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

Ограничивая набор возможных подтипов, разработчикам легче анализировать код и понимать его поведение.

public sealed class Fruit permits Apple, Banana, Orange {
   // Определение класса
}
public final class Apple extends Fruit {
   // Определение класса
}
public final class Banana extends Fruit {
   // Определение класса
}
public final class Orange extends Fruit {
   // Определение класса
}

В этом примере Fruit — это sealed класс, который допускает подклассы Apple, Banana и Orange.

Таким образом мы четко определяем набор допустимых фруктов и повышаем читабельность кода, облегчая его понимание.

Соблюдение контрактов API

Sealed классы также можно использовать для обеспечения соблюдения контрактов API, которые представляют собой набор ожиданий потребителей API в отношении его поведения.

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

public sealed class Vehicle permits Car, Truck, Motorcycle {
   // Определение класса
}
public final class Car extends Vehicle {
   // Определение класса
}
public final class Truck extends Vehicle {
   // Определение класса
}
public final class Motorcycle extends Vehicle {
   // Определение класса
}

В этом примере Vehicle — это sealed класс, который позволяет расширять его классам Car, Truck и Motorcycle.

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

Предотвращение нежелательных расширений подтипов

Наконец, sealed классы также можно использовать для предотвращения нежелательных расширений подтипов.

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

public sealed class PaymentMethod {
   // Определение класса
}
public final class CreditCard extends PaymentMethod {
   // Определение класса
}
public final class DebitCard extends PaymentMethod {
   // Определение класса
}
public class StolenCard extends PaymentMethod {
   // Определение класса
}

В этом примере PaymentMethod является sealed классом, который не позволяет никаким подтипам расширять его.

Это предотвращает создание класса StolenCard, который не соответствует предполагаемому поведению класса PaymentMethod.

Повышение типобезопасности коллекций

Sealed классы также могут способствовать повышению типобезопасности коллекций, которые являются фундаментальной частью языка Java.

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

public sealed interface Animal permits Dog, Cat, Bird {
   // Определение интерфейса
}
public final class Dog implements Animal {
   // Определение класса
}
public final class Cat implements Animal {
   // Определение класса
}
public final class Bird implements Animal {
   // Определение класса
}

В этом примере Animal — это sealed интерфейс, который позволяет реализовать его классам Dog, Cat и Bird.

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

List<!--? extends Animal--> animals = List.of(new Dog(), new Cat(), new Bird());

В этом примере animals — это список, содержащий элементы, которые расширяют интерфейс Animal.

Поскольку Animal — sealed интерфейс, набор возможных элементов в списке четко определен и типобезопасен.

Содействие развитию API

Sealed классы также могут способствовать развитию API, то есть обновлению API путем добавления или удаления фич.

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

public sealed class Animal permits Dog, Cat {
   // Определение класса
}
public final class Dog extends Animal {
   // Определение класса
}
public final class Cat extends Animal {
   // Определение класса
}
public final class Bird extends Animal {
   // Определение класса
}

В этом примере Animal — это sealed класс, который позволяет Dog и Cat расширять его.

Поскольку Animal является sealed классом, добавление нового подтипа Bird будет критическим изменением и потребует изменения версии API.

Таким образом мы гарантируем совместимость изменений API с существующим кодом, что помогает нам поддерживать стабильность кодовой базы.


Вот еще несколько более конкретных и реальных примеров того, как sealed классы можно использовать в Java-разработке:

Представление различных типов сообщений

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

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

public sealed interface Message permits RequestMessage, ResponseMessage {
   // Определение интерфейса
}
public final class RequestMessage implements Message {
   // Определение класса
}
public final class ResponseMessage implements Message {
   // Определение класса
}

В этом примере Message — это sealed интерфейс, который позволяет классам RequestMessage и ResponseMessage реализовать его.

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

Определение набора доменных объектов

В предметно‑ориентированном проектировании (DDD) доменные объекты представляют концепции и сущности предметной области.

Sealed классы могут определять набор доменных объектов и гарантировать, что каждый тип объекта четко определен и имеет ограниченный набор возможных подтипов.

public sealed interface OrderItem permits ProductItem, ServiceItem {
   // Определение интерфейса
}
public final class ProductItem implements OrderItem {
   // Определение класса
}
public final class ServiceItem implements OrderItem {
   // Определение класса
}

В этом примере OrderItem — это sealed интерфейс, который позволяет классам ProductItem и ServiceItem реализовать его.

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

Представление различных типов пользователей

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

public sealed class User permits Customer, Employee, Admin {
   // Определение класса
}
public final class Customer extends User {
   // Определение класса
}
public final class Employee extends User {
   // Определение класса
}
public final class Admin extends User {
   // Определение класса
}

В этом примере User — это sealed класс, который позволяет допускает подклассы Customer, Employee и Admin.

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

Определение ограниченного набора типов ошибок

Во многих системах ошибки сигнализируют о том, что во время выполнения программы что‑то пошло не так.

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

public sealed class Error permits NetworkError, DatabaseError, SecurityError {
   // Определение класса
}
public final class NetworkError extends Error {
   // Определение класса
}
public final class DatabaseError extends Error {
   // Определение класса
}
public final class SecurityError extends Error {
   // Определение класса
}

В этом примере Error — это sealed класс, который позволяет классам NetworkError, DatabaseError и SecurityError расширять его.

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

Определение ограниченного набора HTTP-методов

Во многих веб‑приложениях взаимодействие с веб‑ресурсами происходит посредством HTTP‑методов.

Sealed классы могут определять ограниченный набор HTTP‑методов и гарантировать, что каждый метод четко определен и имеет ограниченный набор возможных подтипов.

public sealed class HttpMethod permits GetMethod, PostMethod, PutMethod {
   // Определение класса
}
public final class GetMethod extends HttpMethod {
   // Определение класса
}
public final class PostMethod extends HttpMethod {
   // Определение класса
}
public final class PutMethod extends HttpMethod {
   // Определение класса
}

В этом примере HttpMethod — это sealed класс, который позволяет классам GetMethod, PostMethod и PutMethod расширять его.

Используя sealed классы для определения ограниченного набора HTTP‑методов, разработчики могут гарантировать, что каждый метод четко определен и имеет ограниченный набор возможных подтипов.

Это может помочь сделать код более удобным в сопровождении и более простым для понимания.

Определение ограниченного набора параметров конфигурации

Во многих системах для управления поведением программы используются специальные параметры конфигурации.

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

public sealed class ConfigurationParameter permits DebugMode, LoggingLevel {
   // Определение класса
}
public final class DebugMode extends ConfigurationParameter {
   // Определение класса
}
public final class LoggingLevel extends ConfigurationParameter {
   // Определение класса
}

В этом примере ConfigurationParameter является sealed классом, который позволяет классам DebugMode и LoggingLevel расширять его.

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

Это может помочь сделать код более удобным в сопровождении и более простым для понимания.

Определение ограниченного набора стратегий доступа к базе данных

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

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

public sealed class DatabaseAccessStrategy permits JdbcStrategy, JpaStrategy, HibernateStrategy {
   // Определение класса
}
public final class JdbcStrategy extends DatabaseAccessStrategy {
   // Определение класса
}
public final class JpaStrategy extends DatabaseAccessStrategy {
   // Определение класса
}
public final class HibernateStrategy extends DatabaseAccessStrategy {
   // Определение класса
}

В этом примере DatabaseAccessStrategy — это sealed класс, который допускает подклассы JdbcStrategy, JpaStrategy и HibernateStrategy.

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

Определение ограниченного набора методов аутентификации

Во многих системах для проверки личности пользователей используется аутентификация.

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

public sealed class AuthenticationMethod permits PasswordMethod, TokenMethod, BiometricMethod {
   // Определение класса
}
public final class PasswordMethod extends AuthenticationMethod {
   // Определение класса
}
public final class TokenMethod extends AuthenticationMethod {
   // Определение класса
}
public final class BiometricMethod extends AuthenticationMethod {
   // Определение класса
}

В этом примере AuthenticationMethod является sealed классом, который позволяет PasswordMethod, TokenMethod и BiometricMethod расширять его.

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

Это может помочь сделать код более удобным в сопровождении и более простым для понимания.

Заключение

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

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

Освоив sealed классы Java, вы сможете вывести свои навыки программирования на новый уровень, создавая более качественное программное обеспечение.


Приглашаем всех желающих на открытое занятие «Введение в Java Collections: списки и O(n)». На занятии вы познакомитесь со списками в Java и изучите особенности их использования, получите ответ на самый популярный вопрос на собеседованиях по Java. В результате занятия будете знать основные особенности списков в java и скорости их работы. Записаться можно по ссылке.

Комментарии (19)


  1. serarhi
    00.00.0000 00:00
    +1

    Мне это больше всего напоминает enum. Только в enum речь идет о экземплярах, а тут о классах.


    1. aleksandy
      00.00.0000 00:00

      Во-первых, enum - константа. Во-вторых, enum , в отличие от класса, никак нельзя параметризовать. Т.е. sealed interface Value<T> можно, а enum Value<T> - нельзя.


    1. TimReset
      00.00.0000 00:00

      То же возникла эта мысль. Особенно на enum из Rust похоже. Вот простой пример:

      https://doc.rust-lang.org/std/option/enum.Option.html
      pub enum Option<T> { None, Some(T), }

      В sealed классе это наверное было бы типа:

      public sealed class Option<T> permits None, Some..

      public final class None extends Option..

      public final class Some<T> extends Option..

      Те типа enum, но который может нести состояние. И тогда тут все фишки enum работают, но можно ещё дополнительно состояния использовать.


    1. Widowan
      00.00.0000 00:00

      Оно и есть, по существу. Мне около полугода назад на реддите один из авторов JVM (вроде как) и доказывал, что основное применение и идея Sealed - создание тип сумм, что отражено в JEP.


  1. auddu_k
    00.00.0000 00:00
    +3

    Если у кого есть примеры практического применения, где прямо нужно, - поделитесь, пожалуйста.


    1. stas1212
      00.00.0000 00:00
      +1

      паттерн матчинг в свитч контсрукциях, когда число наследников заранее известно


      1. auddu_k
        00.00.0000 00:00

        Таким образом мы гарантируем, что свитч покрывает всё варианты? Но в какой-то момент придётся расшириться и гарантия уйдёт, не?


        1. aleksandy
          00.00.0000 00:00

          Гарантия никуда не денется, т.к. после расширения switch перестанет компилироваться. И в него придётся добавить либо новую ветку с новыми типами, либо default-ветку.


          1. auddu_k
            00.00.0000 00:00

            Короче, я пока не нашёл для своих проектов кейса применения всего этого sealed-добра (
            Возможно для каких-то библиотек и то подозрительно


        1. ValeryIvanov
          00.00.0000 00:00

          Гарантия останется, так как компилятор станет ругаться на код, где switch не покрывает всех наследников sealed классов


    1. aleksandy
      00.00.0000 00:00
      +1

      Классический пример - парсер json. Значением (JsonValue) может быть null, число, строка, массив, объект. Соответственно, расширять JsonValue , кроме как обозначенным типам, незачем.


      1. auddu_k
        00.00.0000 00:00

        Ну, тут может быть, да
        Получается для моделирования каких-то ограниченний предметной области ????
        Спасибо


    1. ultrapotato69
      00.00.0000 00:00
      +1

      Когда в библиотеках нужно запретить наследование для пользователя, но самим активно наследоваться. В jooq активно используется.


    1. iBljad
      00.00.0000 00:00
      +2

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


  1. pin2t
    00.00.0000 00:00
    -1

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


    1. v2kxyz
      00.00.0000 00:00
      +2

      Не так уж и очевидно. Во-первых, final классы, от которых вообще нельзя наследоваться, существуют давно. При этом они вполне публичные, например String. Во-вторых, класс желательно проектировать так, чтобы от него было удобно наследоваться, например не вызывать переопредяемые методы в конструкторах, иначе можно получить спецэффекты. В третьих, решение сделать некий класс непубличным, а его наследники финальными и публичными не всегда удобно, т.к. лишает нас возможности использовать полиморфизм.


    1. auddu_k
      00.00.0000 00:00
      -1

      Не, чтоб соответствовать высоким стандартам других языков )))


    1. aleksandy
      00.00.0000 00:00
      +1

      любой public класс хочет чтобы его максимально использовали

      Угу, именно поэтому во многих фреймворках и библиотеках присутствует пакет internal.


    1. aleksandrzhmydyak
      00.00.0000 00:00
      +2

      Package private не спасает от наследования: в своем super.puper.project делаем пакет com.google.xxx и наследуем. Для меня как sealed classes выглядят как enum в для классов,как написал выше @serarhi . Одно из преимуществ в том что не надо придумывать поведение default для несуществующих кейсов Pattern Matching for switch and Sealed Classes