浅析 Java 中的密封类(Sealed Classes) 在 class 文件中是如何实现的
JDK 17 正式支持 密封类(Sealed Classes),那么密封类在 class 文件中是如何实现的呢?本文对此进行探讨。
要点
要点 1: PermittedSubclasses 属性
在密封类 S 的 class 文件(即 S.class)中会有 PermittedSubclasses 属性。这个属性中记录了可以直接 extend/implement S 的类/接口。
要点 2: 密封类 permit 的子类
密封类 S 所 permit 的子类 C 一定是 sealed/final/non-sealed 种情况中的某一个。
这 种情况下, C.class 的特点列举如下 ⬇️
子类 C 是 sealed/final/non-sealed 中的哪一种? | C.class 的特点 |
|---|---|
sealed | C.class 中有 PermittedSubclasses 属性(其中会记录可以直接 extend/implement C 的 类/接口) |
final | C.class 中的 access_flags 中的 ACC_FINAL 这个 bit 是 |
non-sealed | C.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. sealed, non-sealed, and final Classes 小节 提到 ⬇️
It is a compile-time error if a class has a
sealeddirect superclass or asealeddirect superinterface, and is not declaredfinal,sealed, ornon-sealedeither explicitly or implicitly.Thus, an effect of the
sealedkeyword is to force all direct subclasses to explicitly declare whether they arefinal,sealed, ornon-sealed. This avoids accidentally exposing a sealed class hierarchy to unwanted subclassing.
所以密封类 permit 的子类可以分为 sealed/final/non-sealed 种情况。
这 种情况在例子中都出现了,具体情况如下表所示 ⬇️
| 例子 | 说明 | |
|---|---|---|
sealed | Rectangle | Rectangle 类 permit 的子类是:FilledRectangle |
final | Circle, FilledRectangle | |
non-sealed | Square |
密封类(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 文件的内容。
用如下命令可以编译 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
上面的结果的第 行是 ⬇️
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
所以 access_flags 的值为 0x0021 = 0x0020 + 0x0001。
0x0001(即,ACC_PUBLIC)表明要做的事情这个class是public的0x0020(即,ACC_SUPER)比较特殊,从下图中可以看到,Java SE 8及之后,JVM认为所有的class的这个bit都被置位,所以可以先不管这个bit的具体含义。
看了 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。
关于它的详细介绍,请参考 Java Virtual Machine Specification 中的 4.7. Attributes 小节。
由于我们现在只关心 PermittedSubclasses 这个属性,所以直接前往对应的文档 ⬇️
Java Virtual Machine Specification 中的 4.7.31. The PermittedSubclasses Attribute 小节
从下图绿色线上以及绿色框里的文字可以看出, PermittedSubclasses 属性中的确保存了密封类的信息 ⬇️ 绿色框里的文字特别指出了密封类 不是 通过 access_flags 来实现的。
我把这个描述复制到下方 ⬇️
The
PermittedSubclassesattribute 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 的数量是 ⬇️ ( 表示 number_of_classes,即当前类 permit 的直接子类的数量)
用以下命令可以查看 Figure.class 中每个 byte 的值。
od -t x1 Figure.class
Figure.class 文件中的 PermittedSubclasses 属性刚好出现在文件末尾,下图红框里的值就是 PermittedSubclasses 所包含的 个 byte()。
下方的表格展示了这 个 byte 的含义 ⬇️
| 类型 | 用十六进制表示的值 | 用十进制表示的值 | 含义 | |
|---|---|---|---|---|
attribute_name_index | u2 | 0x000d | ||
attribute_length | u4 | 0x00000008 | 表示这个属性还剩 个 byte 那么长 | |
number_of_classes | u2 | 0x0003 | 表示数组长度为 ⬇️ | |
classes 数组 | 数组中有个 u2 元素 | 数组中的值分别是 0x000e, 0x0010, 0x0012 | 数组中的值分别是 14, 16, 18 |
表格中的结果和 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这个class是final的,所以它的access_flags中的ACC_FINAL这个bit是
子类 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 了以下 个直接子类
CircleSquareRectangle
对这些子类而言,只会有如下的 种情况。既然情况 和 情况 都有各自的体现方式,那么情况 就不需要任何特殊的体现方式了。换言之,如果既不是情况 又不是情况 ,那就只能是情况 了。
| 情况编号 | 密封类 permit 的子类 C 是 sealed/final/non-sealed 中的哪一个? | 在 C.class 文件中如何体现 | 本文中的例子 |
|---|---|---|---|
sealed | C.class 中会有 PermittedSubclasses 属性 | Rectangle | |
final | C.class 的 access_flags 中的 ACC_FINAL 这个 bit 是 | Circle, FilledRectangle | |
non-sealed | C.class 不需要任何特殊表示 | Square |