[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 会是 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 1 </math>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 要做的事情 <|-- 吃饭 吃饭 <|-- 吃早饭 吃饭 <|-- 吃午饭 吃饭 <|-- 吃晚饭 要做的事情 <|-- 睡觉 要做的事情 <|-- 运动

密封类的子类只会有 <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 表明 要做的事情 这个 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 属性,而其中的内容刚好就是 吃饭 的那 <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 属性
  • 由于 睡觉 这个 classfinal 的,所以它的 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 子类 Asealed class A.class 中会有 PermittedSubclasses 属性
<math xmlns="http://www.w3.org/1998/Math/MathML"> 2 2 </math>2 子类 Bfinal class B.classaccess_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 子类 Cnon-sealed class C.class 不需要任何特殊表示

参考资料

相关推荐
Hello.Reader2 小时前
一文通关 Proto3完整语法与工程实践
java·linux·数据库·proto3
DashingGuy2 小时前
算法(keep learning)
java·数据结构·算法
时间行者_知行合一2 小时前
Spring如何选择依赖注入方式
java
counting money2 小时前
JAVA泛型基础
java·开发语言·eclipse
田里的水稻2 小时前
C++_数据类型和数据结构
java·数据结构·c++
兔兔西2 小时前
【数据结构、java学习】数组(Array)
java·数据结构·算法
是2的10次方啊2 小时前
并发容器的艺术:从ConcurrentHashMap到BlockingQueue的完美协奏
java
007php0072 小时前
Go语言面试:传值与传引用的区别及选择指南
java·开发语言·后端·算法·面试·golang·xcode
唐叔在学习2 小时前
从MD5到RSA,一文读懂常见的加密算法
后端