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

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. Създаване

Запечатаната функция въвежда няколко нови модификатори и клаузи в Java: запечатани, незапечатани и разрешения .

3.1. Запечатани интерфейси

За да запечатаме интерфейс, можем да приложим запечатания модификатор към неговата декларация. Най разрешителни клаузата след това определя класовете, които са разрешени за прилагане на запечатано интерфейса:

public sealed interface Service permits Car, Truck { int getMaxServiceIntervalInMonths(); default int getMaxDistanceBetweenServicesInKilometers() { return 100000; } }

3.2. Запечатани класове

Подобно на интерфейсите, ние можем да запечатваме класове, като прилагаме същия запечатан модификатор. Най разрешителни клауза трябва да се определи след всеки се простира или сечива клаузи:

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. Всеки разрешен подклас трябва да дефинира модификатор: окончателен , запечатан или незапечатан.

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"); }

Using if-else makes it difficult for the compiler to determine that we covered all permitted subclasses. For that reason, we are throwing a RuntimeException.

In future versions of Java, the client code will be able to use a switch statement instead of if-else (JEP 375).

By using type test patterns, the compiler will be able to check that every permitted subclass is covered. Thus, there will be no more need for a default clause/case.

4. Compatibility

Let's now take a look at the compatibility of sealed classes with other Java language features like records and the reflection API.

4.1. Records

Sealed classes work very well with records. Since records are implicitly final, the sealed hierarchy is even more concise. Let's try to rewrite our class example using records:

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. Reflection

Sealed classes are also supported by the reflection API, where two public methods have been added to the java.lang.Class:

  • The isSealed method returns true if the given class or interface is sealed.
  • Method permittedSubclasses returns an array of objects representing all the permitted subclasses.

We can make use of these methods to create assertions that are based on our example:

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. Conclusion

В тази статия разгледахме запечатани класове и интерфейси, функция за предварителен преглед в Java SE 15. Разгледахме създаването и използването на запечатани класове и интерфейси, както и техните ограничения и съвместимост с други езикови функции.

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

Както винаги, пълният изходен код е достъпен в GitHub.