浅析 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
的子类,会有什么特殊的标记- 如果子类
A
是sealed class
,则A.class
中会有PermittedSubclasses
属性 - 如果子类
B
是final class
,则B.class
中的access_flags
中的ACC_FINAL
这个bit
会是 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 1 </math>1 - 如果子类
C
是non-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 要做的事情 {
}
从这段代码可以看出来,小明其实还是没想好今天到底要做什么。不过我们的重点在于密封类,就别管小明了。这里涉及的类有点多,我画了张类图来表示它们之间的关系 ⬇️
密封类的子类只会有 <math xmlns="http://www.w3.org/1998/Math/MathML"> 3 3 </math>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:
吃饭
运动
睡觉
上面的结果的第 <math xmlns="http://www.w3.org/1998/Math/MathML"> 4 4 </math>4 行是 flags: (0x0021) ACC_PUBLIC, ACC_SUPER
。 所以 access_flags
的值为 0x0021 = 0x0020 + 0x0001
。
0x0001
表明要做的事情
这个class
是public
的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
属性,而其中的内容刚好就是 吃饭
的那 <math xmlns="http://www.w3.org/1998/Math/MathML"> 3 3 </math>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
属性 - 由于
睡觉
这个class
是final
的,所以它的access_flags
中的ACC_FINAL
这个bit
是 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 1 </math>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
了 吃饭, 运动, 睡觉
, 那么对这些子类而言,只会有如下的 <math xmlns="http://www.w3.org/1998/Math/MathML"> 3 3 </math>3 种情况。既然情况 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 1 </math>1 和 情况 <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 2 </math>2 都有各自的体现方式,那么情况 <math xmlns="http://www.w3.org/1998/Math/MathML"> 3 3 </math>3 就不需要任何特殊的体现方式了。换言之,如果既不是情况 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 1 </math>1 又不是情况 <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 2 </math>2,那就只能是情况 <math xmlns="http://www.w3.org/1998/Math/MathML"> 3 3 </math>3 了。
编号 | 子类的具体情况 | 在子类的 class 文件中如何体现 |
---|---|---|
<math xmlns="http://www.w3.org/1998/Math/MathML"> 1 1 </math>1 | 子类 A 是 sealed class |
A.class 中会有 PermittedSubclasses 属性 |
<math xmlns="http://www.w3.org/1998/Math/MathML"> 2 2 </math>2 | 子类 B 是 final class |
B.class 的 access_flags 中的 ACC_FINAL 这个 bit 是 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 1 </math>1 |
<math xmlns="http://www.w3.org/1998/Math/MathML"> 3 3 </math>3 | 子类 C 是 non-sealed class |
C.class 不需要任何特殊表示 |