1. 概述
Java SE 17 引入了密封类(Sealed Classes)这一特性(JEP 409)。
该特性旨在提供更细粒度的继承控制能力。通过密封机制,类和接口可以明确指定哪些子类或实现类是被允许的。
换句话说,一个类或接口现在可以声明只有特定的类才能继承它或实现它。这在领域建模和增强库安全性方面非常有用。
2. 动机
类层次结构能够通过继承实现代码复用,但这并不是类体系结构的唯一目的。
2.1. 领域建模的需求
有时候我们构建类层次结构是为了表达业务中已知的几种可能性。
举个例子,假设我们的业务只处理汽车(Car)和卡车(Truck),不涉及摩托车(Motorcycle)。那么在设计 Vehicle
抽象类时,我们希望只允许 Car
和 Truck
继承它。这样可以确保该抽象类不会被滥用。
在这个场景中,我们更关注的是对已知子类的清晰建模,而不是防御未知的扩展。
在 Java 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
、non-sealed
和 permits
。
3.1. 密封接口
要密封一个接口,可以在其声明前加上 sealed
关键字,并使用 permits
子句列出允许实现它的类:
public sealed interface Service permits Car, Truck {
int getMaxServiceIntervalInMonths();
default int getMaxDistanceBetweenServicesInKilometers() {
return 100000;
}
}
3.2. 密封类
密封类的定义方式类似。只需在类声明前加上 sealed
,并在 extends
或 implements
之后使用 permits
子句:
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
。如果声明为 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. 传统判断方式
通过密封类,客户端代码可以清楚地知道所有允许的子类有哪些。
传统方式是使用 instanceof
和一系列 if-else
判断:
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. 模式匹配优化
借助 模式匹配,我们可以避免显式类型转换,但仍需使用 if-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");
}
⚠️ 使用 if-else
结构时,编译器无法确认是否覆盖了所有可能的子类,因此我们不得不抛出 RuntimeException
。
未来版本中(如 JEP 375),可以使用 switch
表达式替代 if-else
。届时,编译器将能验证是否覆盖了所有允许的子类,从而不再需要 default
分支。
5. 兼容性分析
5.1. 与 Record 的兼容性
密封类与 Record 非常契合。由于 Record 是隐式 final
的,密封类结构会更加简洁。我们尝试将上面的例子改写为 Record:
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;
}
}
5.2. 反射 API 支持
密封类在 反射 API 中也得到了支持,java.lang.Class
中新增了两个方法:
isSealed()
:如果类或接口是密封的,返回true
。getPermittedSubclasses()
:返回一个数组,表示所有允许的子类。
我们可以用这些方法来编写断言验证:
Assertions.assertThat(truck.getClass().isSealed()).isEqualTo(false);
Assertions.assertThat(truck.getClass().getSuperclass().isSealed()).isEqualTo(true);
Assertions.assertThat(truck.getClass().getSuperclass().getPermittedSubclasses())
.contains(Class.forName(truck.getClass().getCanonicalName()));
6. 总结
✅ 在本文中,我们深入探讨了 Java SE 17 中引入的密封类与接口特性。我们介绍了其创建方式、使用方法、限制条件以及与其他语言特性的兼容性。
示例中包括:
- 密封接口和密封类的创建
- 使用密封类进行类型判断(含/不含模式匹配)
- 密封类与 Record、反射 API 的结合使用
完整代码可从 GitHub 仓库 获取。