JEP 409 密封类
增强 Java 编程语言,引入密封类(sealed classes)和密封接口(sealed interfaces)。密封类和接口限制了哪些其他类或接口可以扩展或实现它们。从 Java 15 开始作为预览功能引入,经过改进后,在 Java 16 时作为二次预览版交付,并在 Java 17 中正式成为标准特性(与 16 时相比没有变化)。
密封类和密封接口允许类或接口的作者控制哪些代码负责实现它,提供一种比访问修饰符更声明式的方式来限制超类的使用,为模式匹配的未来发展提供基础,支持模式的详尽分析。
一、背景
面向对象的数据模型,类和接口的继承层次结构,已经被证明在建模现代应用程序处理的真实世界数据方面非常有效。这种表达能力是 Java 语言的一个重要方面。
然而,在某些情况下,这种表达能力会被通过某种方式有效地受到限制。例如,Java 支持使用枚举类来建模一个给定类只有固定数量实例的情况。在下面的代码中,一个枚举类列出了固定的一组行星。它们是该类唯一的值,因此可以在它们之间进行详尽的切换——不需要编写默认子句:
1 | enum Planet { |
使用枚举类来建模固定值集通常是有帮助的,但有时希望建模固定种类的值集合。这可以通过不将类层次结构用作代码继承和重用的机制,而是作为一种列出值种类的方式。基于行星例子,可以这样建模天文学领域中的值种类:
1 | interface Celestial { ... } |
然而,这个层次结构并没有反映出模型中只有三种类型的天体这一重要的领域知识。在这种情况下,限制子类或子接口的集合可以简化建模。
考虑另一个例子:在一个图形库中,Shape 类的作者可能打算只有特定的类可以扩展 Shape,因为库的许多工作涉及以适当的方式处理每种形状。作者关心的是处理已知 Shape 子类的代码清晰度,而不关心编写代码来防御未知的 Shape 子类。允许任意类扩展 Shape 并因此继承其代码用于重用并不是此案例的目标。不幸的是,Java 假设代码重用总是目标:如果 Shape 可以被扩展,那么它可以被任意数量的类扩展。放松这一假设,使得作者能够声明一个不向任意类开放扩展的类层次结构将会很有帮助。在这样的封闭类层次结构内仍然可以进行代码重用,但不能超出该范围。
Java 开发者对限制子类集合的概念并不陌生,因为在 API 设计中经常遇到这种情况。语言在这方面提供的工具有限:要么使类成为 final
,使其没有任何子类;要么使类或其构造函数包私有,使其只能在同一包中拥有子类。JDK 中出现了一个包私有的超类示例:
1 | package java.lang; |
当目标是代码重用时,比如让 AbstractStringBuilder
的子类共享其 append
方法的代码,包私有方法是有用的。然而,当目标是建模替代方案时,这种方法毫无用处,因为用户代码无法访问关键抽象——超类——以对其进行切换。允许用户访问超类而不允许扩展它,除非借助非公共构造函数等脆弱的技巧,否则无法指定这一点——而这对于接口不起作用。在一个声明了 Shape 及其子类的图形库中,如果只有一个包可以访问 Shape 将是不幸的。
PS:JEP 里举例竟然就是 AbstractStringBuilder
,我第一次看到这个 sealed
这个关键字就是你在 StringBuilder
这里。看的源码为 JDK 21,现在已经改为了:
1 | abstract sealed class AbstractStringBuilder |
总之,应该允许超类广泛可访问(因为它代表了对用户的重要的抽象),但不允许广泛可扩展(因为它的子类应限制为作者所知的那些)。这类超类的作者应该能够表达它是与一组特定的子类共同开发的,既为了文档化意图供读者参考,也为了允许 Java 编译器执行这些限制。同时,超类不应该不必要地约束其子类,例如,强迫它们成为 final
或阻止它们定义自己的状态。
二、详情描述
密封类或接口只能由那些被允许扩展或实现它们的类和接口来继承或实现。
通过在类声明中应用 sealed
修饰符,可以将一个类定义为密封类。然后,在任何 extends
和 implements
子句之后,使用 permits
子句指定哪些类被允许扩展密封类。例如,下面对 Shape
的声明指定了三个允许的子类:
1 | package com.example.geometry; |
permits
指定的类必须位于超类附近:如果超类位于命名模块(module)中,则这些类必须在同一模块(module)内;如果超类位于未命名模块中,则这些类必须在同一包内。
例如,在以下 Shape
的声明中,它允许子类都位于同一命名模块的不同包中:
1 | package com.example.geometry; |
当允许的子类数量较少且规模较小时,将它们与密封类一起声明在同一源文件中可能更方便。在这种情况下,密封类可以省略 permits
子句,Java 编译器会从源文件中的声明推断出允许的子类。(这些子类可以是辅助类或嵌套类。)例如,如果以下代码位于 Root.java
中,那么密封类 Root
被推断为有三个允许的子类:
1 | abstract sealed class Root { ... |
permits
指定的类必须具有规范名称,否则会导致编译时错误。这意味着匿名类和局部类不能成为密封类的允许子类型。
密封类对其允许的子类施加了三个限制:
- 模块和包的归属:密封类及其允许的子类必须属于同一个模块,并且如果是在未命名模块中声明的,则必须属于同一个包。
- 直接继承:每个允许的子类必须直接继承密封类。
- 传播密封状态的方式:每个允许的子类必须使用修饰符来描述它如何传播其超类发起的密封:
- 允许的子类可以被声明为
final
以防止其部分的类层次结构进一步扩展。(Record
类隐式地声明为final
。) - 允许的子类可以被声明为
sealed
以允许其部分的层次结构在受限的情况下进一步扩展,超出其密封超类的预期。 - 允许的子类可以被声明为
non-sealed
以使其部分的层次结构重新开放给未知子类的扩展。密封类无法阻止其允许的子类这样做。(non-sealed
是提议用于 Java 的第一个连字符关键字。)
- 允许的子类可以被声明为
作为第三个约束的例子,Circle
和 Square
可能是 final
的,而 Rectangle
是密封的,并且添加了一个新的子类 WeirdShape
,它是非密封的:
1 | package com.example.geometry; |
即使 WeirdShape
对未知类开放扩展,所有这些子类的实例也同样是 WeirdShape
的实例。因此,用来测试 Shape
实例是否为 Circle
、Rectangle
、Square
或 WeirdShape
的代码仍然是详尽无遗的。
每个允许的子类必须精确使用 final
、sealed
和 non-sealed
之一作为修饰符。一个类不可能同时是 sealed
的(意味着有子类)和 final
的(意味着没有子类),或者同时是 non-sealed
的(意味着有子类)和 final
的(意味着没有子类),或者同时是 sealed
的(意味着受限制的子类)和 non-sealed
的(意味着不受限制的子类)。
final
修饰符可以被视为密封的一种特殊情况,其中完全禁止扩展/实现。也就是说,final
在概念上等同于 sealed
加上一个不指定任何内容的 permits
子句,尽管这样的 permits
子句不能被写出。
密封或非密封的类可以是抽象的,并且可以有抽象成员。密封类可以允许抽象的子类,前提是这些子类随后被声明为密封或非密封,而不是最终的。
如果任何类试图扩展密封类但未被允许,这将导致编译时错误。
1、类的可访问性
由于 extends
和 permits
子句使用了类名,允许的子类和其密封超类之间必须是相互可访问的。然而,允许的子类之间不必具有相同的可访问性,也不必与密封类相同。特别是,一个子类可以比密封类更不公开。这意味着,在未来支持模式匹配的版本中,当使用 switch
语句时,如果没有默认子句(或其他全量模式),某些代码将无法详尽地遍历所有子类。Java 编译器将被鼓励检测到 switch
语句不如原作者预期的那样详尽,并定制错误消息建议添加默认子句。
2、密封接口
如同类一样,接口也可以通过应用 sealed
修饰符来密封。在指定超接口的 extends
子句之后,使用 permits
子句指定实现类和子接口。例如,上面提到的行星例子可以重写为:
1 | sealed interface Celestial |
这里有一个经典的类层次结构示例,其中有一组已知的子类:建模数学表达式。
1 | package com.example.expression; |
3、密封和 Record
类
密封类与 Record
类配合得很好。Record
类隐式地声明为 final
,因此由记录类组成的密封层次结构比上述例子稍微简洁一些:
1 | package com.example.expression; |
密封类和记录类的组合有时被称为代数数据类型(algebraic data types):Record
类允许表达产品类型(product types),而密封类允许表达和类型(sum types)。
4、密封和类型转换
类型转换表达式将值转换为类型,而 instanceof
表达式则测试值是否属于某种类型。Java 对这些表达式中允许使用的类型非常宽松。例如:
1 | interface I {} |
这段程序是合法的,即使目前 C
对象不可能实现接口 I
。当然,随着程序的发展,这可能会发生:
1 | class B extends C implements I {} |
类型转换规则捕捉到了开放扩展性的概念。Java 类型系统并不假设封闭世界;类和接口可以在将来某个时间点被扩展,类型转换编译为运行时测试,因此可以安全地保持灵活性。
然而,转换规则确实处理了类绝对不能被扩展的情况,即当它是最终类 (final class
) 的时候。
1 | interface I {} |
方法 test
无法编译,因为编译器知道 C
没有子类,所以既然 C
不实现 I
,那么 C
的值就永远不可能实现 I
。这是一个编译时错误。
如果 C
不是最终类,而是密封类呢?它的直接子类是显式枚举的,并且根据密封类型的定义,它们位于同一模块中,因此期望编译器检查是否有类似的编译时错误。考虑以下代码:
1 | interface I {} |
类 C
不实现 I
,并且不是最终类,按照现有规则,可能得出结论认为转换是可能的。然而,C
是密封的,并且它只有一个允许的直接子类 D
。根据密封类型的定义,D
必须是最终的、密封的或非密封的。在这个例子中,C
的所有直接子类都是最终的,并且不实现 I
。因此,这个程序应该被拒绝,因为不可能存在实现 I
的 C
的子类型。
相比之下,考虑一个类似的程序,其中一个密封类的直接子类是非密封的:
1 | interface I {} |
这是类型正确的,因为 D
的非密封子类型有可能实现 I
。
因此,支持密封类会导致缩小引用转换的定义发生变化,以遍历密封层次结构并在编译时确定哪些转换是不可能的。
5、JDK 中的密封类
密封类在 JDK 中的一个例子是在 java.lang.constant
包中,该包建模了 JVM 实体的描述符:
1 | package java.lang.constant; |
6、密封类与模式匹配
密封类带来的一个重大好处将在 JEP 406 中实现,该提案提议扩展 switch
语句以支持模式匹配。用户代码将能够使用增强模式的 switch
来代替对密封类实例的 if-else
链进行检查。使用密封类将允许 Java 编译器检查模式是否详尽。
例如,考虑以下使用前面声明的密封层次结构的代码:
1 | Shape rotate(Shape shape, double angle) { |
编译器无法确保 instanceof
测试覆盖了所有允许的子类。最后的 else
子句实际上是不可达的,但编译器无法验证这一点。更重要的是,如果省略了 instanceof Rectangle
测试,则不会发出编译时错误消息。
相比之下,使用带有模式匹配的 switch
(JEP 406),编译器可以确认每个允许的子类都被覆盖,因此不需要默认子句或其他模式。此外,如果缺少三个案例中的任何一个,编译器会发出错误消息:
1 | Shape rotate(Shape shape, double angle) { |
7、Java 语法
类声明的语法已更改为如下所示:
1 | NormalClassDeclaration: |
8、JVM 对密封类的支持
Java 虚拟机在运行时识别密封类和接口,并防止未经授权的子类和子接口扩展它们。
虽然 sealed
是一个类修饰符,但在 ClassFile
结构中没有 ACC_SEALED
标志。相反,密封类的类文件有一个 PermittedSubclasses
属性,它隐式地指示了 sealed
修饰符并显式地指定了允许的子类:
1 | PermittedSubclasses_attribute { |
允许的子类列表是强制性的。即使允许的子类是由编译器推断出来的,这些推断出的子类也会明确包含在 PermittedSubclasses
属性中。
被定义的类的类文件不携带任何新的属性。
当 JVM 尝试定义一个其超类或超接口具有 PermittedSubclasses
属性的类时,正在定义的类必须由属性命名。否则,将抛出 IncompatibleClassChangeError
。
9、反射 API
在 java.lang.Class
中添加了以下公共方法:
1 | Class<?>[] getPermittedSubclasses(); |
方法 getPermittedSubclasses()
返回一个包含表示类的允许子类的 java.lang.Class
对象数组,如果类是密封的。如果类不是密封的,则返回空数组。
方法 isSealed
如果给定的类或接口是密封的则返回 true
。(与 isEnum
类比。)
三、依赖关系
密封类不依赖于任何其他的 JEP。如前所述,JEP 406 提议扩展 switch
以支持模式匹配,并基于密封类来改进 switch
的详尽性检查。
相关链接
JEP 360: Sealed Classes (Preview)
JEP 397: Sealed Classes (Second Preview)
OB links
[[JEP 395 Records]]
OB tags
#Java #JDK新特性