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

浅析 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 会是 1 1 1
    • 如果子类 Cnon-sealed class,则 C.class 中不需要任何特殊表示

正文

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

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

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 要做的事情 <|-- 吃饭 吃饭 <|-- 吃早饭 吃饭 <|-- 吃午饭 吃饭 <|-- 吃晚饭 要做的事情 <|-- 睡觉 要做的事情 <|-- 运动

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

子类 特点
吃饭 ⬅️ 它也是一个 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 的无符号数)。

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

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

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

bash 复制代码
javac 要做的事情.java

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

text 复制代码
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:
  吃饭
  运动
  睡觉

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

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

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

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

text 复制代码
PermittedSubclasses:
  吃饭
  运动
  睡觉

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

关于 Attributes 的补充说明

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

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

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

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

从下图绿色线上以及绿色框里的文字可以看出, PermittedSubclasses 属性中的确保存了密封类的信息 ⬇️ 绿色框里的文字特别指出了密封类 不是 通过 access_flags 来实现的。 由于这个描述出自 The Java Virtual Machine Specification,所以来源可靠,我把这个描述复制到下方 ⬇️

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

验证剩余的类
1. 吃饭.class

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

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

bash 复制代码
javap -v -p 吃饭

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

text 复制代码
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 属性,而其中的内容刚好就是 吃饭 的那 3 3 3 个子类。

2. 睡觉.class

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

bash 复制代码
javap -v -p 睡觉

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

text 复制代码
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 这个 bit 1 1 1
3. 运动.class

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

bash 复制代码
javap -v -p 运动

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

text 复制代码
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吃饭, 运动, 睡觉, 那么对这些子类而言,只会有如下的 3 3 3 种情况。既然情况 1 1 1 和 情况 2 2 2 都有各自的体现方式,那么情况 3 3 3 就不需要任何特殊的体现方式了。换言之,如果既不是情况 1 1 1 又不是情况 2 2 2,那就只能是情况 3 3 3 了。

编号 子类的具体情况 在子类的 class 文件中如何体现
1 1 1 子类 Asealed class A.class 中会有 PermittedSubclasses 属性
2 2 2 子类 Bfinal class B.classaccess_flags 中的 ACC_FINAL 这个 bit 1 1 1
3 3 3 子类 Cnon-sealed class C.class 不需要任何特殊表示

参考资料

相关推荐
Penge6662 小时前
Go 接口编译期断言
后端
我是一颗柠檬2 小时前
【MySQL全面教学】MySQL面试高频考点汇总Day15(2026年)
数据库·后端·mysql·面试
橙淮3 小时前
并发编程(六)
java·jvm
拽着尾巴的鱼儿3 小时前
springboot openfeign 自定义feign 接口重试机制
java·spring boot·后端
白露与泡影3 小时前
2026大厂Java面试题大全!牛客网最新版
java·开发语言
Ceelog3 小时前
久坐党自救指南:屏幕前 8 小时,身体到底在经历什么
前端·后端
EntyIU4 小时前
JVM内存与GC笔记
java·jvm·笔记
XS0301064 小时前
并发编程 六
java·后端
yaoxin5211234 小时前
419. 现代 Java IO 最佳实践 - 写入文本文件
java·windows·python
雪宫街道4 小时前
synchronized 锁的范围:对象锁、类锁与代码块锁
java·jvm·后端·面试