[Java] 浅析密封类(Sealed Classes) 在 class 文件中是如何实现的

109 阅读9分钟

浅析 Java 中的密封类(Sealed Classes) 在 class 文件中是如何实现的

JDK 17 正式支持 密封类(Sealed Classes),那么密封类在 class 文件中是如何实现的呢?本文对此进行探讨。

要点

要点 1: PermittedSubclasses 属性

在密封类 Sclass 文件(即 S.class)中会有 PermittedSubclasses 属性。这个属性中记录了可以直接 extend/implement S 的类/接口。

要点 2: 密封类 permit 的子类

密封类 Spermit 的子类 C 一定是 sealed/final/non-sealed 33 种情况中的某一个。

33 种情况下, C.class 的特点列举如下 ⬇️

子类 Csealed/final/non-sealed 中的哪一种?C.class 的特点
sealedC.class 中有 PermittedSubclasses 属性(其中会记录可以直接 extend/implement C 的 类/接口)
finalC.class 中的 access_flags 中的 ACC_FINAL 这个 bit11
non-sealedC.class 中不需要任何特殊标记

正文

准备工作:一个密封类的例子

Java Language Updates 中的 6 Sealed Classes 一文 中有密封类的例子,我把其中一个例子复制过来 ⬇️ (有微小的变动)

public sealed class Figure
    // The permits clause has been omitted
    // as its permitted classes have been
    // defined in the same file.
{ }

final class Circle extends Figure {
    float radius;
}
non-sealed class Square extends Figure {
    float side;
}
sealed class Rectangle extends Figure {
    float length, width;
}
final class FilledRectangle extends Rectangle {
    int red, green, blue;
}

请将代码保存为 Figure.java

我画了类图来表示这些类之间的关系 ⬇️

classDiagram
Figure <|-- Circle
Figure <|-- Square
Figure <|-- Rectangle
Rectangle <|-- FilledRectangle
class Circle {
    float radius
}
class Square {
    float side
}
class Rectangle {
    float length
    float width
}
class FilledRectangle {
    int red
    int green
    int blue
}

Java Language Specification 中的 8.1.1.2. sealednon-sealed, and final Classes 小节 提到 ⬇️

It is a compile-time error if a class has a sealed direct superclass or a sealed direct superinterface, and is not declared finalsealed, or non-sealed either explicitly or implicitly.

Thus, an effect of the sealed keyword is to force all direct subclasses to explicitly declare whether they are finalsealed, or non-sealed. This avoids accidentally exposing a sealed class hierarchy to unwanted subclassing.

所以密封类 permit 的子类可以分为 sealed/final/non-sealed 33 种情况。 这 33 种情况在例子中都出现了,具体情况如下表所示 ⬇️

例子说明
sealedRectangleRectanglepermit 的子类是:FilledRectangle
finalCircle, FilledRectangle
non-sealedSquare

密封类(Sealed Classes) 在 class 文件中是如何实现的

现在我们来分析密封类(Sealed Classes)在 class 文件中是如何实现的。一个猜测是,class 文件中可能会用 access_flags 中的某一个 bit 来表示这个 class 是密封类。 说到这里,先补充一下 access_flags 具体是什么。

关于 access_flags 的补充说明

Java Virtual Machine Specification 中的 4.1. The ClassFile Structure 小节 提到了 class 文件的结构 ⬇️ 在下图中绿色箭头所示位置,可以看到 access_flags (可以将 u2 简单理解成 2 byte 的无符号数)。

image.png

Java Virtual Machine Specification 中的 4.1. The ClassFile Structure 小节 可以找到如下的表格,其中说明了 access_flags 中每个 bit 的含义。

image.png

这个表格中并没有和密封类(Sealed Classes)直接相关的 bit。莫非理解有误?我们再去看看 class 文件的内容。

用如下命令可以编译 Figure.java。编译后会得到若干个 class 文件

javac Figure.java

javap -v -p Figure 命令可以查看 Figure.class 文件的具体内容。 结果如下(开头几行略去) ⬇️

public class Figure
  minor version: 0
  major version: 66
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #7                          // Figure
  super_class: #2                         // java/lang/Object
  interfaces: 0, fields: 0, methods: 1, attributes: 2
Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Class              #8             // Figure
   #8 = Utf8               Figure
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               SourceFile
  #12 = Utf8               Figure.java
  #13 = Utf8               PermittedSubclasses
  #14 = Class              #15            // Circle
  #15 = Utf8               Circle
  #16 = Class              #17            // Square
  #17 = Utf8               Square
  #18 = Class              #19            // Rectangle
  #19 = Utf8               Rectangle
{
  public Figure();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0
}
SourceFile: "Figure.java"
PermittedSubclasses:
  Circle
  Square
  Rectangle

上面的结果的第 44 行是 ⬇️

flags: (0x0021) ACC_PUBLIC, ACC_SUPER

所以 access_flags 的值为 0x0021 = 0x0020 + 0x0001

  • 0x0001 (即,ACC_PUBLIC)表明 要做的事情 这个 classpublic
  • 0x0020 (即,ACC_SUPER)比较特殊,从下图中可以看到,Java SE 8 及之后,JVM 认为所有的 class 的这个 bit 都被置位,所以可以先不管这个 bit 的具体含义。 image.png

看了 class 文件中的 access_flags 后,可以确认,密封类不是通过 access_flags 来实现的

上文已经展示了 javap -v -p Figure 命令的完整结果,我们再找找是否有其他内容包含了密封类的信息。其中最后几行是这样 ⬇️

PermittedSubclasses:
  Circle
  Square
  Rectangle

这部分属于 class 文件的属性(Attributes)部分。说到这里,先补充一下 Attributes 具体是什么。

关于 Attributes 的补充说明

Java Virtual Machine Specification 中的 4.1. The ClassFile Structure 小节 提到了 class 文件的结构 ⬇️ 在下图中绿色箭头所示位置,可以看到 Attributes

image.png

关于它的详细介绍,请参考 Java Virtual Machine Specification 中的 4.7. Attributes 小节

由于我们现在只关心 PermittedSubclasses 这个属性,所以直接前往对应的文档 ⬇️

Java Virtual Machine Specification 中的 4.7.31. The PermittedSubclasses Attribute 小节

从下图绿色线上以及绿色框里的文字可以看出, PermittedSubclasses 属性中的确保存了密封类的信息 ⬇️ 绿色框里的文字特别指出了密封类 不是 通过 access_flags 来实现的。 image.png

我把这个描述复制到下方 ⬇️

The PermittedSubclasses attribute records the classes and interfaces that are authorized to directly extend or implement the current class or interface (§5.3.5).

查看 PermittedSubclasses 属性各个 byte 的值

Java Virtual Machine Specification 中的 4.7.31. The PermittedSubclasses Attribute 小节 中可以找到 PermittedSubclasses 属性的具体格式 ⬇️

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

在这里,可以将 u2/u4 简单理解为 2 byte/4 byte 的无符号数。 从上方的格式可以推算出,一个 PermittedSubclasses 属性中 byte 的数量是 ⬇️ (nn 表示 number_of_classes,即当前类 permit 的直接子类的数量)

2+4+2+2×n2+4+2+2\times n

用以下命令可以查看 Figure.class 中每个 byte 的值。

od -t x1 Figure.class

Figure.class 文件中的 PermittedSubclasses 属性刚好出现在文件末尾,下图红框里的值就是 PermittedSubclasses 所包含的 1414byte(2+4+2+2×3=142+4+2+2\times3 =14)。

image.png

下方的表格展示了这 1414byte 的含义 ⬇️

类型用十六进制表示的值用十进制表示的值含义
attribute_name_indexu20x000d1313image.png
attribute_lengthu40x0000000888表示这个属性还剩 88byte 那么长
number_of_classesu20x000333表示数组长度为 33 ⬇️
classes 数组数组中有33u2 元素数组中的值分别是 0x000e, 0x0010, 0x0012数组中的值分别是 14, 16, 18image.png

表格中的结果和 javap -v -p Figure 命令给出的结果是一致的。

验证密封类的子类的结构
子类 1: Rectangle

既然密封类的信息是保存在 PermittedSubclasses 属性中的,那么在 Rectangle.class 中应该也能找到 PermittedSubclasses 属性。我们来验证一下。

用如下的命令可以查看 Rectangle.class 的内容 ⬇️

javap -v -p Rectangle

结果如下(开头几行略去) ⬇️

class Rectangle extends Figure
  minor version: 0
  major version: 66
  flags: (0x0020) ACC_SUPER
  this_class: #7                          // Rectangle
  super_class: #2                         // Figure
  interfaces: 0, fields: 2, methods: 1, attributes: 2
Constant pool:
   #1 = Methodref          #2.#3          // Figure."<init>":()V
   #2 = Class              #4             // Figure
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               Figure
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Class              #8             // Rectangle
   #8 = Utf8               Rectangle
   #9 = Utf8               length
  #10 = Utf8               F
  #11 = Utf8               width
  #12 = Utf8               Code
  #13 = Utf8               LineNumberTable
  #14 = Utf8               SourceFile
  #15 = Utf8               Figure.java
  #16 = Utf8               PermittedSubclasses
  #17 = Class              #18            // FilledRectangle
  #18 = Utf8               FilledRectangle
{
  float length;
    descriptor: F
    flags: (0x0000)

  float width;
    descriptor: F
    flags: (0x0000)

  Rectangle();
    descriptor: ()V
    flags: (0x0000)
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method Figure."<init>":()V
         4: return
      LineNumberTable:
        line 13: 0
}
SourceFile: "Figure.java"
PermittedSubclasses:
  FilledRectangle

最后确实有 PermittedSubclasses 属性,而其中的内容刚好就是 Rectangle permit 的那个子类 FilledRectangle

子类 2: Circle

用如下的命令可以查看 Circle.class 的内容 ⬇️

javap -v -p Circle

结果如下(开头几行略去) ⬇️

final class Circle extends Figure
  minor version: 0
  major version: 66
  flags: (0x0030) ACC_FINAL, ACC_SUPER
  this_class: #7                          // Circle
  super_class: #2                         // Figure
  interfaces: 0, fields: 1, methods: 1, attributes: 1
Constant pool:
   #1 = Methodref          #2.#3          // Figure."<init>":()V
   #2 = Class              #4             // Figure
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               Figure
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Class              #8             // Circle
   #8 = Utf8               Circle
   #9 = Utf8               radius
  #10 = Utf8               F
  #11 = Utf8               Code
  #12 = Utf8               LineNumberTable
  #13 = Utf8               SourceFile
  #14 = Utf8               Figure.java
{
  float radius;
    descriptor: F
    flags: (0x0000)

  Circle();
    descriptor: ()V
    flags: (0x0000)
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method Figure."<init>":()V
         4: return
      LineNumberTable:
        line 7: 0
}
SourceFile: "Figure.java"
  • 由于 Circle 这个 class 没有被 sealed 修饰,所以 Circle.class 没有 PermittedSubclasses 属性
  • 由于 Circle 这个 classfinal 的,所以它的 access_flags 中的 ACC_FINAL 这个 bit11
子类 3: Square

用如下的命令可以查看 Square.class 的内容 ⬇️

javap -v -p Square

结果如下(开头几行略去) ⬇️

class Square extends Figure
  minor version: 0
  major version: 66
  flags: (0x0020) ACC_SUPER
  this_class: #7                          // Square
  super_class: #2                         // Figure
  interfaces: 0, fields: 1, methods: 1, attributes: 1
Constant pool:
   #1 = Methodref          #2.#3          // Figure."<init>":()V
   #2 = Class              #4             // Figure
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               Figure
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Class              #8             // Square
   #8 = Utf8               Square
   #9 = Utf8               side
  #10 = Utf8               F
  #11 = Utf8               Code
  #12 = Utf8               LineNumberTable
  #13 = Utf8               SourceFile
  #14 = Utf8               Figure.java
{
  float side;
    descriptor: F
    flags: (0x0000)

  Square();
    descriptor: ()V
    flags: (0x0000)
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method Figure."<init>":()V
         4: return
      LineNumberTable:
        line 10: 0
}
SourceFile: "Figure.java"

这次没有任何特殊的内容。那么 non-sealed 是如何体现的呢?可以这样想,Figure 这个 class permit 了以下 33 个直接子类

  • Circle
  • Square
  • Rectangle

对这些子类而言,只会有如下的 33 种情况。既然情况 11 和 情况 22 都有各自的体现方式,那么情况 33 就不需要任何特殊的体现方式了。换言之,如果既不是情况 11 又不是情况 22,那就只能是情况 33 了。

情况编号密封类 permit 的子类 Csealed/final/non-sealed 中的哪一个?C.class 文件中如何体现本文中的例子
11sealedC.class 中会有 PermittedSubclasses 属性Rectangle
22finalC.classaccess_flags 中的 ACC_FINAL 这个 bit11Circle, FilledRectangle
33non-sealedC.class 不需要任何特殊表示Square

参考资料