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. Ограничения
Запечатанный класс накладывает три важных ограничения на его разрешенные подклассы:
- Все разрешенные подклассы должны принадлежать к тому же модулю, что и запечатанный класс.
- Каждый разрешенный подкласс должен явно расширять запечатанный класс.
- Каждый разрешенный подкласс должен определять модификатор: 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 .