了解 Java 的密封类
提到密封类,需要首先介绍Java的继承机制。
Java的继承机制的设计理念是开放的。在满足可见性限制的前提下,类默认都是可以被继承的。
这种开放的继承机制在有些情况下会产生问题。比如,子类用来列举设计中可能出现的情况。考虑下面的继承体系结构:
形状类下的子类有圆形、长方形和三角形。这表示了系统中所支持的形状类型。在开放继承时,如果使用这个库的代码添加了另外一个形状类的子类,比如六边形,可能产生运行时的错误,因为库的代码中并没有相应的支持。
在引入密封类之前,Java中有两种做法可以对继承进行限制。
- 第一种做法是把类声明为
final
,禁止任何子类。 - 第二种做法是使用包可见的构造器,允许来自同一个 Java 包的子类。
这两种做法都存在一定的局限性。
密封类在 Java 15 中以预览功能的形式引入,在 Java 16 再次预览,在 Java 17 中成为正式功能。
密封类由 sealed
修饰符和 permits
子句组成。sealed
修饰符表示当前类是密封的,permits
则显式的列出来全部允许的子类。比如,下面代码中的 Shape
类。Shape
只允许3个子类,Circle
、Rectangle
和 Triangle
。
public abstract sealed class Shape permits Circle, Rectangle, Triangle {
}
public final class Circle extends Shape {
}
public final class Rectangle extends Shape {
}
public final class Triangle extends Shape {
}
可以在 permits
中出现的子类需要满足两个限制:
- 如果父类在命名模块中,这些类必须在同一个模块中。
- 如果父类在未命名模块中,这些类必须在同一个包中。
如果子类比较简单,可以把子类和父类放在同一个 Java 源文件中。此时可以省略 permits
子句,而由编译器来自动推断。
public abstract sealed class Shape {
final class Circle extends Shape {
}
final class Rectangle extends Shape {
}
final class Triangle extends Shape {
}
}
每个被允许的子类需要使用修饰符来描述如何往下传递密封行为。一共有3种选择:
- 声明为
final
来禁止继续继承。 - 声明为
sealed
以同样的方式来限制继承。 - 声明为
non-sealed
来恢复为开放继承。
这3个修饰符必须选择其一。
下面的代码展示了相关的用法。
public abstract sealed class Shape {
final class Circle extends Shape {
}
sealed class Rectangle extends Shape {
final class Square extends Rectangle {
}
}
non-sealed class FreeFormShape extends Shape {
}
}
接口也可以被声明为 sealed
, permits
子句中可以包含子接口和实现类。下面的代码给出了示例。
public sealed interface Shape {
record Point(double x, double y) {
}
record Circle(Point center, double radius) implements Shape {
}
record Rectangle(Point topLeft, double width, double height) implements
Shape {
}
}
由于记录类型都是声明为 final
的。记录类型很适合作为密封类或接口的子类或实现。
java.lang.Class
中新增了两个与密封类相关的方法。isSealed()
判断 Class
对象是否表示密封类或接口。getPermittedSubclasses()
返回允许的子类的数组。
在Java类文件中,新的属性 PermittedSubclasses
用来记录允许的子类。当 JVM 定义 Java 类时,如果当前类的父类有该属性,则当前类必须出现在该属性中,否则JVM会抛出 IncompatibleClassChangeError
错误。
属性 PermittedSubclasses
的结构如下所示。
PermittedSubclasses_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 number_of_classes;
u2 classes[number_of_classes];
}