新倩女幽魂最新互通版
1.87 GB · 2025-11-13
Java 中的密封类(Sealed Classes) 在 class 文件中是如何实现的JDK 17 正式支持 密封类(Sealed Classes),那么密封类在 class 文件中是如何实现的呢?本文对此进行探讨。
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 会是 C 是 non-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
要做的事情 <|-- 吃饭
吃饭 <|-- 吃早饭
吃饭 <|-- 吃午饭
吃饭 <|-- 吃晚饭
要做的事情 <|-- 睡觉
要做的事情 <|-- 运动
密封类的子类只会有 种情况,这个例子里都出现了,具体情况如下表所示 ⬇️
| 子类 | 特点 |
|---|---|
吃饭 | ⬅️ 它也是一个 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 文件
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:
吃饭
运动
睡觉
上面的结果的第 行是 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 要做的事情 命令的完整结果,考虑到它比较短,我们可以在里面找找是否有其他内容包含了密封类的信息。
这个结果的最后几行如下 ⬇️
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,所以来源可靠,我把这个描述复制到下方 ⬇️
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( 表示 number_of_classes,即当前类 permit 的直接子类的数量)。
由于 PermittedSubclasses 属性刚好在 要做的事情.class 文件的末尾,所以用比较暴力的方法也可以查看它的内容。
我们可以用以下命令来查看 要做的事情.class 中的每个 byte 的值。
od -t x1 要做的事情.class
下图红框里的值就是 PermittedSubclasses 对应的 个 byte()。
下方的表格展示了这 个 byte 的含义 ⬇️ 我们可以看到这里的结果和 javap -v -p 要做的事情 命令给出的结果是一致的。
| 类型 | 用十六进制表示的值 | 用十进制表示的值 | 含义 | |
|---|---|---|---|---|
attribute_name_index | u2 | 0x000d | ||
attribute_length | u4 | 0x00000008 | 表示这个属性还剩 个 byte 那么长 | |
number_of_classes | u2 | 0x0003 | 表示数组长度为 ⬇️ | |
classes 数组 | 数组中有个 u2 元素 | 数组中的值分别是 0x000e, 0x0010, 0x0012 | 数组中的值分别是 14, 16, 18 |
吃饭.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 属性,而其中的内容刚好就是 吃饭 的那 个子类。
睡觉.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 属性睡觉 这个 class 是 final 的,所以它的 access_flags 中的 ACC_FINAL 这个 bit 是 运动.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 了 吃饭, 运动, 睡觉,
那么对这些子类而言,只会有如下的 种情况。既然情况 和 情况 都有各自的体现方式,那么情况 就不需要任何特殊的体现方式了。换言之,如果既不是情况 又不是情况 ,那就只能是情况 了。
| 编号 | 子类的具体情况 | 在子类的 class 文件中如何体现 |
|---|---|---|
子类 A 是 sealed class | A.class 中会有 PermittedSubclasses 属性 | |
子类 B 是 final class | B.class 的 access_flags 中的 ACC_FINAL 这个 bit 是 | |
子类 C 是 non-sealed class | C.class 不需要任何特殊表示 |
ClassFile StructurePermittedSubclasses Attribute
1.87 GB · 2025-11-13
367.43 MB · 2025-11-13
1.78 GB · 2025-11-13