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

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

要点

  • 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 属性中记录了密封类的信息,但是精准的表述请读者朋友参考这里的英文)
  • 密封类所 permit 的子类,会有什么特殊的标记
    • 如果子类 Asealed class,则 A.class 中会有 PermittedSubclasses 属性
    • 如果子类 Bfinal class,则 B.class 中的 access_flags 中的 ACC_FINAL 这个 bit 会是 11
    • 如果子类 Cnon-sealed class,则 C.class 中不需要任何特殊表示

正文

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

小明学习了密封类(Sealed Classes)的知识后,决定应用所学的知识写点代码。早上醒来后,小明信心满满,决定规划一下今天要做的事情,于是写了这样的代码 ⬇️ (请将以下代码保存为 要做的事情.java,不过正常情况下还是不要用有汉字的类名,这里的例子仅供娱乐)

public sealed class 要做的事情 permits 吃饭, 运动, 睡觉 {
}

sealed class 吃饭 extends 要做的事情 {
}

final class 吃早饭 extends 吃饭 {
}

final class 吃午饭 extends 吃饭 {
}

final class 吃晚饭 extends 吃饭 {
}

final class 睡觉 extends 要做的事情 {

}

// 还没想好要从事哪种运动,所以就让“运动”是 non-sealed class 吧
non-sealed class 运动 extends 要做的事情 {
}

从这段代码可以看出来,小明其实还是没想好今天到底要做什么。不过我们的重点在于密封类,就别管小明了。这里涉及的类有点多,我画了张类图来表示它们之间的关系 ⬇️

classDiagram
要做的事情 <|-- 吃饭
吃饭 <|-- 吃早饭
吃饭 <|-- 吃午饭
吃饭 <|-- 吃晚饭
要做的事情 <|-- 睡觉
要做的事情 <|-- 运动

密封类的子类只会有 33 种情况,这个例子里都出现了,具体情况如下表所示 ⬇️

子类特点
吃饭⬅️ 它也是一个 sealed class,它 permit 的子类是:吃早饭吃午饭吃晚饭
睡觉⬅️ 它是 final class
运动⬅️ 它是 non-sealed class

密封类(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 文件。

用如下命令可以编译 要做的事情.java。编译后会得到若干个 class 文件

javac 要做的事情.java

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

public class 要做的事情
  minor version: 0
  major version: 66
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #7                          // 要做的事情
  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             // 要做的事情
   #8 = Utf8               要做的事情
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               SourceFile
  #12 = Utf8               要做的事情.java
  #13 = Utf8               PermittedSubclasses
  #14 = Class              #15            // 吃饭
  #15 = Utf8               吃饭
  #16 = Class              #17            // 运动
  #17 = Utf8               运动
  #18 = Class              #19            // 睡觉
  #19 = Utf8               睡觉
{
  public 要做的事情();
    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: "要做的事情.java"
PermittedSubclasses:
  吃饭
  运动
  睡觉

上面的结果的第 44 行是 flags: (0x0021) ACC_PUBLIC, ACC_SUPER。 所以 access_flags 的值为 0x0021 = 0x0020 + 0x0001

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

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

上文已经展示了 javap -v -p 要做的事情 命令的完整结果,考虑到它比较短,我们可以在里面找找是否有其他内容包含了密封类的信息。 这个结果的最后几行如下 ⬇️

PermittedSubclasses:
  吃饭
  运动
  睡觉

这部分看起来属于 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 Java Virtual Machine Specification,所以来源可靠,我把这个描述复制到下方 ⬇️

查看 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 属性的长度会是 2+4+2+2×n2+4+2+2times nbyte(nn 表示 number_of_classes,即当前类 permit 的直接子类的数量)。

由于 PermittedSubclasses 属性刚好在 要做的事情.class 文件的末尾,所以用比较暴力的方法也可以查看它的内容。 我们可以用以下命令来查看 要做的事情.class 中的每个 byte 的值。

od -t x1 要做的事情.class

下图红框里的值就是 PermittedSubclasses 对应的 1414byte(2+4+2+2×3=142+4+2+2times3 =14)。

image.png

下方的表格展示了这 1414byte 的含义 ⬇️ 我们可以看到这里的结果和 javap -v -p 要做的事情 命令给出的结果是一致的。

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

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

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

javap -v -p 吃饭

主要的结果如下(开头的几行我略去了) ⬇️

class 吃饭 extends 要做的事情
  minor version: 0
  major version: 66
  flags: (0x0020) ACC_SUPER
  this_class: #7                          // 吃饭
  super_class: #2                         // 要做的事情
  interfaces: 0, fields: 0, methods: 1, attributes: 2
Constant pool:
   #1 = Methodref          #2.#3          // 要做的事情."<init>":()V
   #2 = Class              #4             // 要做的事情
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               要做的事情
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Class              #8             // 吃饭
   #8 = Utf8               吃饭
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               SourceFile
  #12 = Utf8               要做的事情.java
  #13 = Utf8               PermittedSubclasses
  #14 = Class              #15            // 吃早饭
  #15 = Utf8               吃早饭
  #16 = Class              #17            // 吃午饭
  #17 = Utf8               吃午饭
  #18 = Class              #19            // 吃晚饭
  #19 = Utf8               吃晚饭
{
  吃饭();
    descriptor: ()V
    flags: (0x0000)
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method 要做的事情."<init>":()V
         4: return
      LineNumberTable:
        line 4: 0
}
SourceFile: "要做的事情.java"
PermittedSubclasses:
  吃早饭
  吃午饭
  吃晚饭

最后确实有 PermittedSubclasses 属性,而其中的内容刚好就是 吃饭 的那 33 个子类。

2. 睡觉.class

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

javap -v -p 睡觉

主要的结果如下(开头的几行我略去了) ⬇️

final class 睡觉 extends 要做的事情
  minor version: 0
  major version: 66
  flags: (0x0030) ACC_FINAL, ACC_SUPER
  this_class: #7                          // 睡觉
  super_class: #2                         // 要做的事情
  interfaces: 0, fields: 0, methods: 1, attributes: 1
Constant pool:
   #1 = Methodref          #2.#3          // 要做的事情."<init>":()V
   #2 = Class              #4             // 要做的事情
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               要做的事情
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Class              #8             // 睡觉
   #8 = Utf8               睡觉
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               SourceFile
  #12 = Utf8               要做的事情.java
{
  睡觉();
    descriptor: ()V
    flags: (0x0000)
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method 要做的事情."<init>":()V
         4: return
      LineNumberTable:
        line 16: 0
}
SourceFile: "要做的事情.java"
  • 由于 睡觉 这个 class 没有被 sealed 修饰,所以 睡觉.class 没有 PermittedSubclasses 属性
  • 由于 睡觉 这个 classfinal 的,所以它的 access_flags 中的 ACC_FINAL 这个 bit11
3. 运动.class

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

javap -v -p 运动

主要的结果如下(开头的几行我略去了) ⬇️

class 运动 extends 要做的事情
  minor version: 0
  major version: 66
  flags: (0x0020) ACC_SUPER
  this_class: #7                          // 运动
  super_class: #2                         // 要做的事情
  interfaces: 0, fields: 0, methods: 1, attributes: 1
Constant pool:
   #1 = Methodref          #2.#3          // 要做的事情."<init>":()V
   #2 = Class              #4             // 要做的事情
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               要做的事情
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Class              #8             // 运动
   #8 = Utf8               运动
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               SourceFile
  #12 = Utf8               要做的事情.java
{
  运动();
    descriptor: ()V
    flags: (0x0000)
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method 要做的事情."<init>":()V
         4: return
      LineNumberTable:
        line 21: 0
}
SourceFile: "要做的事情.java"

这次没有任何特殊的内容。那么 non-sealed 是如何体现的呢? 可以这样想,既然 要做的事情 这个 class permit吃饭, 运动, 睡觉, 那么对这些子类而言,只会有如下的 33 种情况。既然情况 11 和 情况 22 都有各自的体现方式,那么情况 33 就不需要任何特殊的体现方式了。换言之,如果既不是情况 11 又不是情况 22,那就只能是情况 33 了。

编号子类的具体情况在子类的 class 文件中如何体现
11子类 Asealed classA.class 中会有 PermittedSubclasses 属性
22子类 Bfinal classB.classaccess_flags 中的 ACC_FINAL 这个 bit11
33子类 Cnon-sealed classC.class 不需要任何特殊表示

参考资料

  • The Java Virtual Machine Specification 中的
    • 4.1. The ClassFile Structure
    • 4.7. Attributes
    • 4.7.31. The PermittedSubclasses Attribute
本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:[email protected]