Рубрики
Без рубрики

Запечатанные классы и интерфейсы в Java 15

Исследуйте закрытые классы и интерфейсы, функцию предварительного просмотра в Java SE 15.

Автор оригинала: baeldung.

1. Обзор

Выпуск Java SE 15 вводит запечатанные классы ( JEP 360 ) в качестве функции предварительного просмотра.

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

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

2. Мотивация

Иерархия классов позволяет нам повторно использовать код с помощью наследования. Однако иерархия классов может иметь и другие цели. Повторное использование кода-это здорово, но не всегда является нашей основной целью.

2.1. Возможности моделирования

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

В качестве примера представьте себе бизнес-домен, который работает только с легковыми и грузовыми автомобилями, а не с мотоциклами. При создании абстрактного класса Vehicle в Java мы должны иметь возможность разрешить его расширение только классам Car и Truck . Таким образом, мы хотим убедиться, что в нашем домене не будет неправильного использования абстрактного класса Vehicle .

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

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

2.2. Пакетно-Частный Подход

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

Конечный класс не может иметь подклассов. Частный класс пакета может иметь только подклассы в одном пакете.

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

public class Vehicles {

    abstract static class Vehicle {

        private final String registrationNumber;

        public Vehicle(String registrationNumber) {
            this.registrationNumber = registrationNumber;
        }

        public String getRegistrationNumber() {
            return registrationNumber;
        }

    }

    public static final class Car extends Vehicle {

        private final int numberOfSeats;

        public Car(int numberOfSeats, String registrationNumber) {
            super(registrationNumber);
            this.numberOfSeats = numberOfSeats;
        }

        public int getNumberOfSeats() {
            return numberOfSeats;
        }

    }

    public static final class Truck extends Vehicle {

        private final int loadCapacity;

        public Truck(int loadCapacity, String registrationNumber) {
            super(registrationNumber);
            this.loadCapacity = loadCapacity;
        }

        public int getLoadCapacity() {
            return loadCapacity;
        }

    }

}

2.3. Суперкласс Доступен, Но Не Расширяется

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

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

3. Создание

Функция sealed вводит несколько новых модификаторов и предложений в Java: sealed, non-sealed, и разрешения .

3.1. Герметичные интерфейсы

Чтобы запечатать интерфейс, мы можем применить модификатор sealed к его объявлению. Затем в предложении permits указываются классы, которым разрешено реализовывать запечатанный интерфейс:

public sealed interface Service permits Car, Truck {

    int getMaxServiceIntervalInMonths();

    default int getMaxDistanceBetweenServicesInKilometers() {
        return 100000;
    }

}

3.2. Запечатанные классы

Подобно интерфейсам, мы можем запечатать классы, применив тот же модификатор sealed . Предложение permits должно быть определено после любых предложений extends или implements :

public abstract sealed class Vehicle permits Car, Truck {

    protected final String registrationNumber;

    public Vehicle(String registrationNumber) {
        this.registrationNumber = registrationNumber;
    }

    public String getRegistrationNumber() {
        return registrationNumber;
    }

}

Разрешенный подкласс должен определять модификатор. Он может быть объявлен окончательным, чтобы предотвратить любые дальнейшие расширения:

public final class Truck extends Vehicle implements Service {

    private final int loadCapacity;

    public Truck(int loadCapacity, String registrationNumber) {
        super(registrationNumber);
        this.loadCapacity = loadCapacity;
    }

    public int getLoadCapacity() {
        return loadCapacity;
    }

    @Override
    public int getMaxServiceIntervalInMonths() {
        return 18;
    }

}

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

public non-sealed class Car extends Vehicle implements Service {

    private final int numberOfSeats;

    public Car(int numberOfSeats, String registrationNumber) {
        super(registrationNumber);
        this.numberOfSeats = numberOfSeats;
    }

    public int getNumberOfSeats() {
        return numberOfSeats;
    }

    @Override
    public int getMaxServiceIntervalInMonths() {
        return 12;
    }

}

3.4. Ограничения

Запечатанный класс накладывает три важных ограничения на его разрешенные подклассы:

  1. Все разрешенные подклассы должны принадлежать к тому же модулю, что и запечатанный класс.
  2. Каждый разрешенный подкласс должен явно расширять запечатанный класс.
  3. Каждый разрешенный подкласс должен определять модификатор: final , sealed или |/non-sealed.

4. Использование

4.1. Традиционный Способ

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

Традиционный способ рассуждать о подклассе-использовать набор операторов if-else и instanceof проверок:

if (vehicle instanceof Car) {
    return ((Car) vehicle).getNumberOfSeats();
} else if (vehicle instanceof Truck) {
    return ((Truck) vehicle).getLoadCapacity();
} else {
    throw new RuntimeException("Unknown instance of Vehicle");
}

4.2. Сопоставление шаблонов

Применяя сопоставление с шаблоном , мы можем избежать дополнительного приведения класса, но нам все еще нужен набор операторов i f-else :

if (vehicle instanceof Car car) {
    return car.getNumberOfSeats();
} else if (vehicle instanceof Truck truck) {
    return truck.getLoadCapacity();
} else {
    throw new RuntimeException("Unknown instance of Vehicle");
}

Использование i f-else затрудняет компилятору определение того, что мы охватили все разрешенные подклассы. По этой причине мы создаем исключение RuntimeException .

В будущих версиях Java клиентский код сможет использовать оператор switch вместо i f-else ( JEP 375 ).

Используя type test patterns , компилятор сможет проверить, что охвачен каждый разрешенный подкласс. Таким образом, больше не будет необходимости в предложении default /case.

4. Совместимость

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

4.1. Записи

Запечатанные классы очень хорошо работают с записями . Поскольку записи неявно являются окончательными, запечатанная иерархия еще более лаконична. Давайте попробуем переписать наш пример класса с помощью записей:

public sealed interface Vehicle permits Car, Truck {

    String getRegistrationNumber();

}

public record Car(int numberOfSeats, String registrationNumber) implements Vehicle {

    @Override
    public String getRegistrationNumber() {
        return registrationNumber;
    }

    public int getNumberOfSeats() {
        return numberOfSeats;
    }

}

public record Truck(int loadCapacity, String registrationNumber) implements Vehicle {

    @Override
    public String getRegistrationNumber() {
        return registrationNumber;
    }

    public int getLoadCapacity() {
        return loadCapacity;
    }

}

4.2. Отражение

Запечатанные классы также поддерживаются API reflection , где в java.lang были добавлены два открытых метода.Класс:

  • Метод isSealed возвращает true , если данный класс или интерфейс запечатан.
  • Метод разрешенные подклассы возвращает массив объектов, представляющих все разрешенные подклассы.

Мы можем использовать эти методы для создания утверждений, основанных на нашем примере:

Assertions.assertThat(truck.getClass().isSealed()).isEqualTo(false);
Assertions.assertThat(truck.getClass().getSuperclass().isSealed()).isEqualTo(true);
Assertions.assertThat(truck.getClass().getSuperclass().permittedSubclasses())
  .contains(ClassDesc.of(truck.getClass().getCanonicalName()));

5. Заключение

В этой статье мы рассмотрели запечатанные классы и интерфейсы, функцию предварительного просмотра в Java SE 15. Мы рассмотрели создание и использование закрытых классов и интерфейсов, а также их ограничения и совместимость с другими языковыми функциями.

В примерах мы рассмотрели создание запечатанного интерфейса и запечатанного класса, использование запечатанного класса (с сопоставлением шаблонов и без него) и совместимость запечатанных классов с записями и API отражения.

Как всегда, полный исходный код доступен на GitHub .