Java 15中的密封類和接口

1.概述

Java SE 15的發行版引入了密封類( JEP 360 )作為預覽功能。

此功能是關於在Java中啟用更細粒度的繼承控制。密封允許類和接口定義其允許的子類型。

換句話說,一個類或接口現在可以定義哪些類可以實現或擴展它。對於域建模和增加庫的安全性而言,這是一個有用的功能。

2.動機

類層次結構使我們能夠通過繼承重用代碼。但是,類層次結構也可以有其他用途。代碼重用固然很棒,但並非始終是我們的主要目標。

2.1 建模可能性

類層次結構的替代目的可以是對域中存在的各種可能性進行建模。

例如,假設一個業務領域僅適用於汽車和卡車,而不適用於摩托車。在Vehicle抽像類時,我們應該只允許CarTruck類擴展它。這樣,我們要確保在我們的域內Vehicle

在此示例中,我們更關注代碼處理已知子類的清晰度,而不是防御所有未知子類

在版本15之前,Java假定代碼重用始終是目標。每個類都可以擴展為任意數量的子類。

2.2 打包私有方法

在早期版本中,Java在繼承控制方面提供了有限的選項。

最終類不能有子類。程序包專用類只能在同一程序包中具有子類。

使用package-private方法,用戶無法訪問抽像類,同時也不允許他們擴展它:

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中引入了兩個新的修飾符和子句: sealed, non-sealed,permits

3.1 密封接口

要密封接口,我們可以將sealed修飾符應用於其聲明。然後, permits子句指定允許實現密封接口的類:

public sealed interface Service permits Car, Truck {



 int getMaxServiceIntervalInMonths();



 default int getMaxDistanceBetweenServicesInKilometers() {

 return 100000;

 }



 }

3.2 密封類

與接口類似,我們可以通過應用相同的sealed修飾符來密封類。 permits子句應在任何extendsimplements子句之後定義:

public abstract sealed class Vehicle permits Car, Truck {



 protected final String registrationNumber;



 public Vehicle(String registrationNumber) {

 this.registrationNumber = registrationNumber;

 }



 public String getRegistrationNumber() {

 return registrationNumber;

 }



 }

允許的子類必須定義一個修飾符。為了防止進一步擴展,可以將其聲明為final

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;

 }



 }

允許的子類也可以聲明為sealed 。但是,如果我們聲明它是non-sealed,那麼它可以擴展:

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. 每個允許的子類都必須定義一個修飾符: finalsealed 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 felse( JEP 375 )。

通過使用類型測試模式,編譯器將能夠檢查是否覆蓋了所有允許的子類。因此,將不再需要default子句/情況。

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也支持密封的類,其中兩個公共方法已添加到java.lang.Class:

  • 如果給定的類或接口是密封的,則isSealed方法將返回true
  • 方法permittedSubclasses返回一個對像數組,該對象表示所有允許的子類。

我們可以利用這些方法來創建基於我們示例的斷言:

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的兼容性。