1.概述
Java SE 15
的发行版引入了密封类( JEP 360 )作为预览功能。
此功能是关于在Java中启用更细粒度的继承控制。密封允许类和接口定义其允许的子类型。
换句话说,一个类或接口现在可以定义哪些类可以实现或扩展它。对于域建模和增加库的安全性而言,这是一个有用的功能。
2.动机
类层次结构使我们能够通过继承重用代码。但是,类层次结构也可以有其他用途。代码重用固然很棒,但并非始终是我们的主要目标。
2.1 建模可能性
类层次结构的替代目的可以是对域中存在的各种可能性进行建模。
例如,假设一个业务领域仅适用于汽车和卡车,而不适用于摩托车。在Vehicle
抽像类时,我们应该只允许Car
和Truck
类扩展它。这样,我们要确保在我们的域内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
子句应在任何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;
}
}
允许的子类必须定义一个修饰符。为了防止进一步扩展,可以将其声明为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 约束条件
密封类对其允许的子类施加三个重要的约束:
- 所有允许的子类都必须与密封类属于同一模块。
- 每个允许的子类都必须显式扩展密封的类。
- 每个允许的子类都必须定义一个修饰符:
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
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的兼容性。
0 评论