详解 Java 17 中的密封类(Sealed Classes)

密封类和接口(Sealed Classes)在 Java 15 中以预览功能的形式引入,在 Java 16 中再次预览,最后在 Java 17 中成为正式功能。密封类会对 Java 中的继承机制产生影响。

Java 现有的继承机制

继承机制是 Java 语言的面向对象编程中的重要组成部分。子类可以继承父类的状态和行为。Java 中的类默认都是可以被继承的,除非被声明为 final。

Java 的这种做法是值得商榷的。Kotlin 就采取了相反的做法。Kotlin 中的类默认是 final 的,除非被声明为 open。

在密封类出现之前,Java 语言并没有提供灵活的方式来对继承机制进行限制。一般就只有两种选择:

  • 把类声明为 final,然后不允许任何子类。
  • 通过限制类的构造方法的可见性来对继承进行限制。
  • 第一种选择就是一刀切,没有什么灵活性,直接禁止继承。第二种选择其实是一种取巧的做法。因为子类的构造方法必须调用父类的构造方法,如果一个类的构造方法不可见,那么自然就无法继承自这个类。

    在使用继承机制时,一个典型的需求是,只允许自己的代码进行继承,而不允许别人的代码来继承。这么说可能有点晦涩,让我们通过一个具体的例子来说明。假设我们要开发一个图形绘制系统。该系统中支持不同类型的图形,包括长方形、正方形和圆形。我们很容易就可以得出下面这样的继承层次结构。在这个继承层次结构中,Shape(形状)是基类,有两个子类 Rectangle (长方形)和圆形(Circle),Rectangle 有子类 Square(正方形)。

    形状类的层次结构

    我们希望可以自己添加 Shape 的子类,来不断增强图形绘制系统的功能,比如可以添加 Shape 的子类 Triangle 来支持三角形。但是我们不希望其他人可以随意地添加 Shape 的子类。因为每一个 Shape 子类的添加,都需要有图形绘制系统后台的支持。如果在图形绘制系统的 classpath 上出现了一个别人添加的 Shape 子类,那么可能会产生一些安全风险。比如,图形绘制系统可能通过查找 Shape 的所有子类的方式来识别系统中可用的形状,这可能会导致带有恶意代码的类被加载。

    一种可能的做法是把 Shape 的构造方法声明为包私有(package private),这样只允许来自同一个 Java 包中的子类。不过这种声明更多地的是示意性的,意思是这个类并不是被设计为可公开继承的。这种限制并不是强制性的,我们仍然可以创建这些类的子类,只需要把子类的包名设置成与要继承的类一致就行了。

    密封类介绍

    密封类允许开发人员显式地指定所允许的子类。这是通过 sealed 修饰符和 permits 子句来实现的。在下面的代码中,Shape 类声明为 sealed,通过 permits 声明了允许的子类 Circle、Rectangle 和 Triangle。除了这3个子类之外,其他的 Shape 的子类都是不允许的。

    public abstract sealed class Shape permits Circle, Rectangle, Triangle {}

    可以出现在 permits 中的子类有一些限制:

  • 如果父类出现在命名模块中,子类必须与父类属于同一个命名模块。
  • 如果父类出现在未命名模块中,子类必须与父类属于同一个包。
  • 如果子类的代码比较少,可以把子类和父类放在同一个源代码文件中。这样就不需要添加 permits 子句,而是由编译器自动推断出允许的子类。只有在同一个源文件中的子类才会被允许。在下面的代码中,Shape 类的子类都声明在同一个 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,允许继续往下进行不受限的继承。
  • final、sealed 和 non-sealed 这3个修饰符必须且只能出现一次,否则会出现编译错误。

    在下面的代码中,Shape 的子类 Circle 声明为 final;Rectangle 声明为 sealed;FreeFormShape 声明为 non-sealed。

    public abstract sealed class Shape permits Circle, Rectangle, FreeFormShape {}final class Circle extends Shape {}public sealed class Rectangle extends Shape {  final class Square extends Rectangle {  }}public non-sealed class FreeFormShape extends Shape {}

    密封接口与密封类是相似的,在接口上添加 sealed 修饰符,限制一个接口所允许的实现类和子接口。下面的代码给出了密封接口的示例。

    public sealed interface Expression permits ConstantExpr, DynamicExpr {}public final class ConstantExpr implements Expression {}public non-sealed interface DynamicExpr extends Expression {}

    Java 的记录类型都是隐式声明为 final 的,因此记录类型很适合与密封类一同使用。下面的代码给出了记录类型与密封类的使用示例,代码很简洁。

    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 {  }}

    密封类的内部细节

    在密封类引入之后,Java 的反射API也进行了修改。java.lang.Class 类增加了两个新的方法:

  • boolean isSealed() 判断当前 Class 对象是否表示密封类或接口。
  • Class<?>[] getPermittedSubclasses() 返回包含了密封类或接口所允许的子类或子接口的数组。
  • 在 JVM 方面,字节代码中在 ClassFile 结构中新增了一个 PermittedSubclasses 属性,用来记录允许的子类。PermittedSubclasses 属性的格式如下所示,其中记录了全部允许的类。

    PermittedSubclasses_attribute {    u2 attribute_name_index;    u4 attribute_length;    u2 number_of_classes;    u2 classes[number_of_classes];}

    当 JVM 尝试加载一个类时,如果它的父类或父接口的定义中包含了 PermittedSubclasses 属性,那么当前类必须出现在 PermittedSubclasses 属性的数组中,否则 JVM 会抛出
    IncompatibleClassChangeError 错误。

    声明:本站部分文章内容及图片转载于互联 、内容不代表本站观点,如有内容涉及侵权,请您立即联系本站处理,非常感谢!

    (0)
    上一篇 2022年8月25日
    下一篇 2022年8月25日

    相关推荐