JEP 409:Sealed Class
对官方文档的不完全翻译,有自己的理解,如有错误请指正。全文参考https://openjdk.org/jeps/409
摘要
使用密封的类和接口增强 Java 编程语言。密封的类和接口限制了哪些其他类(子类)或接口可以扩展或实现它们(父类、父接口)。
目标(密封类的目的?)
允许类或接口的作者控制负责实现它的代码。(只有作者允许的类才可以继承超类)
提供比访问修饰符更具声明性的方式来限制超类的使用。(更细致的限制哪些类可以继承超类)
未来将支持 switch (JEP 406) 的模式匹配。
动机(为什么需要密封类?)
Java 支持枚举类来模拟给定类只有固定数量的实例的情况。使用枚举类对固定值集建模通常很有帮助例如,枚举类列出了一组固定的行星。
enum Planet { MERCURY, VENUS, EARTH }
Planet p = ...
switch (p) {
case MERCURY: ...
case VENUS: ...
case EARTH: ...
}
但有时我们希望对固定的类别集建模,我们可以通过使用类层次结构来做到这一点,这里使用类的层次结构不是为了继承和重用,而是为了列出各种值。例如我们可以使用类的层次结构对天体的各种类型进行建模,但是这种层次结构并不能反映天体只包含Planet 、Star、Comet这三种类别。
interface Celestial { ... }
final class Planet implements Celestial { ... }
final class Star implements Celestial { ... }
final class Comet implements Celestial { ... }
考虑另一个例子:在图形库中,Shape 类的作者可能打算只有特定的类可以扩展 Shape,因为该库的大部分工作都涉及以适当的方式处理每种形状。作者的目标是不允许任意类扩展 Shape 并因此继承其代码以供重用。在Java 中,如果 Shape 完全可以扩展,那么它可以被任意数量的类扩展。但是作者希望声明一个不开放给任意类扩展的类层次结构。在这样一个封闭的类层次结构中,代码重用仍然是可能的,但不能超出。
在Java 中限制子类集的方法有两个:要么使类成为最终类,因此它有零个子类;要么使类或其构造函数成为包私有的,因此它只能在同一个包中具有子类。 JDK 中出现了一个包私有超类的示例:
package java.lang;
abstract class AbstractStringBuilder { ... }
public final class StringBuffer extends AbstractStringBuilder { ... }
public final class StringBuilder extends AbstractStringBuilder { ... }
当目标是代码重用时,包私有方法(超类的访问修饰符为default)很有用,例如让 AbstractStringBuilder 的子类共享其代码以进行追加。然而,当目标是建模时,这种方法(超类的访问修饰符为default)是无用的,因为用户代码无法访问超类。要想允许用户访问超类而不允许用户扩展超类,只有使用非公共构造函数(但这种技巧很脆弱,而且不适用接口)。例如,在声明 Shape 及其子类的图形库中,如果只有一个包可以访问 Shape,那将是不幸的。
总之,超类应该可以被广泛访问(因为它代表了用户的重要抽象)但不能广泛扩展(因为它的子类应该限于作者已知的那些)。这样一个超类的作者应该能够表示它是与一组给定的子类共同开发的,既可以为读者记录意图,也可以让 Java 编译器强制执行。同时,超类不应过分约束其子类,例如强制它们为最终状态(final)或阻止它们定义自己的状态(包私有的构造函数)。
密封类的语法
类声明的语法修改如下:
NormalClassDeclaration:
{ClassModifier} class TypeIdentifier [TypeParameters]
[Superclass] [Superinterfaces] [PermittedSubclasses] ClassBody
ClassModifier:
(one of)
Annotation public protected private
abstract static sealed final non-sealed strictfp
PermittedSubclasses:
permits ClassTypeList
ClassTypeList:
ClassType {, ClassType}
描述(密封类详细描述)
密封类或接口只能由允许的类和接口扩展或实现。
通过将密封修饰符来声明密封类。然后,在extends和implements之后,使用permits指定允许扩展密封类的类。例如,以下 Shape 声明指定了三个允许的子类:
package com.example.geometry;
public abstract sealed class Shape
permits Circle, Rectangle, Square { ... }
permits 指定的类所在的位置:在同一个模块中或在同一个包中。例如,在以下 Shape 声明中,其允许的子类都位于同一命名模块的不同包中:
package com.example.geometry;
public abstract sealed class Shape
permits com.example.polar.Circle,
com.example.quad.Rectangle,
com.example.quad.simple.Square { ... }
当permits的子类的大小和数量较小时,可以将它们声明在与密封类相同的源文件中,同时可以省略permits语句,Java 编译器将从源文件中的声明推断出permits的子类。 (子类可能是辅助类或嵌套类。)例如,如果在 Root.java 中找到以下代码,则推断密封类 Root 具有三个允许的子类:
abstract sealed class Root { ...
final class A extends Root { ... }
final class B extends Root { ... }
final class C extends Root { ... }
}
permits 指定的类必须有一个规范的名称,否则会报告编译时错误。这意味着匿名类和局部类不能成为密封类的子类型。
密封类对其允许的子类施加三个约束:
- 密封类及其允许的子类必须属于同一个模块,并且如果在未命名的模块中声明,则必须属于同一个包。
- 每个允许的子类都必须直接继承密封类。
- 每个允许的子类都必须使用修饰符来描述它如何传播由其超类发起的密封:
- 允许的子类可以声明为 final 以防止其在类层次结构中的一部分被进一步扩展(允许的子类无法再扩展)。 (Record类被隐式声明为最终类。)
- 允许的子类可以再次被声明为密封类(sealed),这样允许的子类可以以一种受限制的方式进一步扩展。
- 允许的子类可以声明为非密封的(non-sealed),这样允许的子类可以任意的扩展(未知的子类也可以扩展)。密封类不能阻止其允许的子类声明为non-sealed。
作为第三个约束的示例,Circle 和 Square 可能是 final 而 Rectangle 是密封的,我们添加一个新的子类 WeirdShape,它是非密封的:
package com.example.geometry;
public abstract sealed class Shape
permits Circle, Rectangle, Square, WeirdShape { ... }
public final class Circle extends Shape { ... }
public sealed class Rectangle extends Shape
permits TransparentRectangle, FilledRectangle { ... }
public final class TransparentRectangle extends Rectangle { ... }
public final class FilledRectangle extends Rectangle { ... }
public final class Square extends Shape { ... }
public non-sealed class WeirdShape extends Shape { ... }
即使 WeirdShape 对未知类的扩展开放,这些子类的所有实例也是 WeirdShape 的实例。因此,编写用于测试 Shape 实例是圆形、矩形、方形还是 WeirdShape 的代码仍然是详尽无遗的。
每个允许的子类必须使用修饰符 final、sealed 和 non-sealed 中的一个。一个类不可能既是密封的(暗示子类)又是最终的(暗示没有子类),或者既是非密封的(暗示子类)又是最终的(暗示没有子类),或者既是密封的(暗示受限制的子类)又是非封闭的-sealed(暗示不受限制的子类)。
final修饰符可以被认为是密封的一种特殊情况,完全禁止扩展/实现。也就是说,final在概念上等同于sealed加上一个不指定任何内容的permits子句,尽管不能编写这样的许可子句。
// 这样写报错
public abstract sealed class Shape permits {
}
密封或非密封的类可以是抽象的,并且具有抽象成员。密封类可以允许抽象的子类,前提是抽象的子类是sealed的或non-sealed的,而不是final的。
// 正确
public non-sealed abstract class TransparentRectangle extends Rectangle {
}
// 错误
public abstract final class TransparentRectangle extends Rectangle {
}
类的可访问性
因为 extends 和 permit 子句使用类名,所以允许的子类和它的密封超类必须可以相互访问。但是,允许的子类不需要彼此具有相同的可访问性,也不必与密封类具有相同的可访问性。特别是,子类可能比密封类的访问权限更小。这意味着,在将来的版本中,当switch支持模式匹配时,除非使用default子句,否则无法覆盖全部的子类。(原因:假设Circle是default的,由于在包外无法访问Circle,而模式匹配要求必须覆盖所有子类,所以只能使用default语句。参考下面的“密封类和模式匹配”)
密封接口
和密封类相似,可以使用sealed 修饰符来密封接口,使用permits指定允许的实现类和子接口。例如,上面的行星示例可以重写如下:
sealed interface Celestial
permits Planet, Star, Comet { ... }
final class Planet implements Celestial { ... }
final class Star implements Celestial { ... }
final class Comet implements Celestial { ... }
这是另一个类层次结构的经典示例,其中有一组已知的子类:建模数学表达式。
package com.example.expression;
public sealed interface Expr
permits ConstantExpr, PlusExpr, TimesExpr, NegExpr { ... }
public final class ConstantExpr implements Expr { ... }
public final class PlusExpr implements Expr { ... }
public final class TimesExpr implements Expr { ... }
public final class NegExpr implements Expr { ... }
密封和记录类
密封类与记录类(record classes)配合得很好。记录类是隐式final的(不可以用abstract修饰),因此记录类的密封层次结构比上面的示例更简洁:
package com.example.expression;
public sealed interface Expr
permits ConstantExpr, PlusExpr, TimesExpr, NegExpr { ... }
public record ConstantExpr(int i) implements Expr { ... }
public record PlusExpr(Expr a, Expr b) implements Expr { ... }
public record TimesExpr(Expr a, Expr b) implements Expr { ... }
public record NegExpr(Expr e) implements Expr { ... }
密封类和记录类的组合有时被称为代数数据类型:记录类允许我们表示乘积类型,密封类允许我们表示和类型。
密封类和类型转换
final和instanceof,如下两个例子:
这个程序是合法的,因为尽管 C 对象目前不可能实现接口 I,但是以后C对象可能实现接口I。
interface I {}
class C {} // does not implement I
void test (C c) {
if (c instanceof I)
System.out.println("It's an I");
}
这个程序中的方法 test 无法编译,因为编译器知道C不可能有子类,由于 C 没有实现 I,所以 C 永远不可能实现 I。这是一个编译时错误。
interface I {}
final class C {}
void test (C c) {
if (c instanceof I) // Compile-time error!
System.out.println("It's an I");
}
在密封类中会怎么样?
C 是密封的并且有一个permits的直接子类D。根据密封类型的定义,D 必须是最终的、密封的或非密封的。在这个例子中,C 的所有直接子类都是 final 的并且没有实现 I。因此这个程序无法编译,因为不可能有 C 的子类型实现 I。
interface I {}
sealed class C permits D {}
final class D extends C {}
void test (C c) {
if (c instanceof I) // Compile-time error!
System.out.println("It's an I");
}
这个程序是正确的,因为非密封类型 D 的子类型可以实现 I。
interface I {}
sealed class C permits D, E {}
non-sealed class D extends C {}
final class E extends C {}
void test (C c) {
if (c instanceof I)
System.out.println("It's an I");
}
JDK 中的密封类
package java.lang.constant;
public sealed interface ConstantDesc
permits String, Integer, Float, Long, Double,
ClassDesc, MethodTypeDesc, DynamicConstantDesc { ... }
// ClassDesc is designed for subclassing by JDK classes only
public sealed interface ClassDesc extends ConstantDesc
permits PrimitiveClassDescImpl, ReferenceClassDescImpl { ... }
final class PrimitiveClassDescImpl implements ClassDesc { ... }
final class ReferenceClassDescImpl implements ClassDesc { ... }
// MethodTypeDesc is designed for subclassing by JDK classes only
public sealed interface MethodTypeDesc extends ConstantDesc
permits MethodTypeDescImpl { ... }
final class MethodTypeDescImpl implements MethodTypeDesc { ... }
// DynamicConstantDesc is designed for subclassing by user code
public non-sealed abstract class DynamicConstantDesc implements ConstantDesc { ... }
密封类和模式匹配
密封类的一个显着好处将在 JEP 406 中实现,它建议使用模式匹配扩展 switch。用户代码将能够使用带有模式增强的switch,而不是使用 if-else 链检查密封类的实例。使用密封类将允许 Java 编译器检查模式是否详尽。
使用if-else的例子:
Shape rotate(Shape shape, double angle) {
if (shape instanceof Circle) return shape;
else if (shape instanceof Rectangle) return shape;
else if (shape instanceof Square) return shape;
else throw new IncompatibleClassChangeError();
}
Java 编译器不能确保 instanceof 测试覆盖所有允许的 Shape 子类。最后的 else 子句实际上是无法到达的,但是编译器无法验证这一点。更重要的是,如果省略了 instanceof Rectangle 测试,则不会发出编译时错误消息。
相反,通过 switch (JEP 406) 的模式匹配,编译器可以确认每个允许的 Shape 子类都被覆盖,因此不需要默认子句或其他总模式。此外,如果缺少以下三种情况中的任何一种,编译器将发出错误消息:‘switch’ statement does not cover all possible input values
Shape rotate(Shape shape, double angle) {
return switch (shape) { // pattern matching switch
case Circle c -> c;
case Rectangle r -> shape.rotate(angle);
case Square s -> shape.rotate(angle);
// no default needed!
}
}
JVM 对密封类的支持
Java 虚拟机在运行时识别密封的类和接口,并防止未经授权的子类和子接口进行扩展。
尽管sealed 是一个类修饰符,但ClassFile 结构中没有ACC_SEALED 标志。相反,密封类的类文件具有 PermittedSubclasses 属性,该属性隐式指示密封修饰符并显式指定允许的子类:
PermittedSubclasses_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 number_of_classes;
u2 classes[number_of_classes];
}
允许的子类列表是强制性的。即使编译器推断出允许的子类,这些推断出的子类也会显式包含在 PermittedSubclasses 属性中。
允许的子类的类文件不携带新属性。
当 JVM 尝试定义其超类或超接口具有 PermittedSubclasses 属性的类时,所定义的类必须由该属性命名。否则,将引发 IncompatibleClassChangeError。
反射 API
我们将以下公共方法添加到 java.lang.Class:
Class<?>[] getPermittedSubclasses()
boolean isSealed()
getPermittedSubclasses() 方法返回一个包含 java.lang.Class 对象的数组,该对象表示该类的允许子类(如果该类是密封的)。如果是非密封的类,则返回一个空数组。
如果给定的类或接口是密封的,则方法 isSealed 返回 true。