Menu
Coddy logo textTech

Sealed-классы (Java 17+)

Часть раздела Объектно-ориентированное программирование путешествия по Java на Coddy — урок 52 из 87.

В традиционной Java, когда вы создаете класс, любой другой класс может его расширить (если только вы не пометите его как final). Но что, если вы хотите разрешить наследование, но не всем подряд? В Java 17 появились запечатанные классы, чтобы дать вам точный контроль над тем, какие классы могут расширять ваш.

Запечатанный класс (sealed class) явно объявляет свои разрешенные подклассы, используя ключевые слова sealed и permits:

sealed class Shape permits Circle, Rectangle, Triangle {
    abstract double area();
}

final class Circle extends Shape {
    double radius;
    double area() { return Math.PI * radius * radius; }
}

final class Rectangle extends Shape {
    double width, height;
    double area() { return width * height; }
}

final class Triangle extends Shape {
    double base, height;
    double area() { return 0.5 * base * height; }
}

Только Circle, Rectangle и Triangle могут расширять Shape. Любой другой класс, пытающийся расширить его, вызовет ошибку компиляции.

Каждый разрешенный подкласс должен использовать один из трех модификаторов: final (дальнейшее расширение запрещено), sealed (продолжает цепочку ограничений) или non-sealed (открывает возможность для неограниченного расширения):

sealed class Vehicle permits Car, Truck { }

final class Car extends Vehicle { }           // Не может быть расширен
non-sealed class Truck extends Vehicle { }    // Кто угодно может расширить Truck

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

challenge icon

Задание

Легко

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

Вы организуете свой код в четырех файлах:

  • Notification.java: Создайте запечатанный абстрактный класс (sealed abstract class), который служит основой для всех уведомлений. Он должен разрешать ровно три подкласса: EmailNotification, SMSNotification и PushNotification. Вашему запечатанному классу необходимо защищенное поле message (String), конструктор для его инициализации, геттер для сообщения и абстрактный метод deliver(), который возвращает String, описывающую способ отправки уведомления.
  • EmailNotification.java: Создайте final класс, который расширяет Notification. EmailNotification имеет дополнительное приватное поле recipient (String) для адреса электронной почты. Его конструктор должен принимать и сообщение, и получателя, вызывая родительский конструктор с помощью super(). Метод deliver() должен возвращать: "Sending email to " + recipient + ": " + message"
  • SMSNotification.java: Создайте non-sealed класс, который расширяет Notification — это позволит другим классам расширять его в будущем, если потребуется. SMSNotification имеет дополнительное приватное поле phoneNumber (String). Его конструктор принимает сообщение и номер телефона. Метод deliver() должен возвращать: "Sending SMS to " + phoneNumber + ": " + message"
  • PushNotification.java: Создайте final класс, который расширяет Notification. PushNotification имеет дополнительное приватное поле deviceId (String). Его конструктор принимает сообщение и идентификатор устройства. Метод deliver() должен возвращать: "Sending push to device " + deviceId + ": " + message"
  • Main.java: Объедините вашу систему уведомлений! Вы получите четыре входных значения: сообщение (String), адрес электронной почты (String), номер телефона (String) и идентификатор устройства (String).

    Создайте по одному экземпляру каждого типа уведомлений, используя одно и то же сообщение, но соответствующие контактные данные. Сохраните все три объекта в массиве типа Notification[], чтобы продемонстрировать полиморфизм с запечатанными классами. Затем переберите массив и выведите результат вызова deliver() для каждого уведомления.

Вы получите четыре входных значения по порядку: сообщение, адрес электронной почты, номер телефона и идентификатор устройства.

Обратите внимание, как запечатанный класс ограничивает иерархию — только три разрешенных класса могут расширять Notification. Каждый подкласс должен быть объявлен как final, sealed или non-sealed. Это дает вам полный контроль над иерархией типов, сохраняя при этом возможность использования полиморфизма!

Шпаргалка

Запечатанный класс (sealed class) ограничивает список классов, которые могут его расширять, используя ключевые слова sealed и permits:

sealed class Shape permits Circle, Rectangle, Triangle {
    abstract double area();
}

Только классы, перечисленные в permits, могут расширять запечатанный класс. Любой другой класс, пытающийся его расширить, вызовет ошибку компиляции.

Каждый разрешенный подкласс должен использовать один из трех модификаторов:

  • final — предотвращает дальнейшее расширение
  • sealed — продолжает цепочку ограничений со своими собственными разрешенными подклассами
  • non-sealed — открывает иерархию для неограниченного расширения
sealed class Vehicle permits Car, Truck { }

final class Car extends Vehicle { }           // Не может быть расширен
non-sealed class Truck extends Vehicle { }    // Кто угодно может расширить Truck

Запечатанные классы полезны для моделирования фиксированного набора типов в предметной области, таких как способы оплаты, типы ответов или геометрические фигуры. Они хорошо работают с сопоставлением с образцом (pattern matching) в выражениях switch, так как компилятор знает все возможные подтипы.

Попробуйте сами

import java.util.Scanner;

class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        
        // Прочитать четыре входных значения
        String message = scanner.nextLine();
        String email = scanner.nextLine();
        String phoneNumber = scanner.nextLine();
        String deviceId = scanner.nextLine();
        
        // TODO: Создать по одному уведомлению каждого типа, используя одно и то же сообщение
        // - EmailNotification с сообщением и email
        // - SMSNotification с сообщением и phoneNumber
        // - PushNotification с сообщением и deviceId
        
        // TODO: Сохранить все три уведомления в массив Notification[]
        // Это демонстрирует полиморфизм с запечатанными (sealed) классами
        
        // TODO: Перебрать массив и вывести результат выполнения deliver()
        // для каждого уведомления
        
    }
}
quiz iconПроверьте себя

В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.

Все уроки раздела Объектно-ориентированное программирование