跳到主要内容

JEP 360:密封类(预览版)

概括

_使用密封类和接口_增强 Java 编程语言。密封类和接口限制其他类或接口可以扩展或实现它们。这是JDK 15 中的预览语言功能。

目标

  • 允许类或接口的作者控制哪些代码负责实现它。
  • 提供比访问修饰符更具声明性的方式来限制超类的使用。
  • 通过支持模式的_详尽分析来支持_模式匹配的未来方向。

非目标

  • 提供新形式的访问控制(例如“朋友”)并不是目标。
  • final以任何方式改变都不是目标。

动机

在 Java 中,类层次结构通过继承实现了代码的重用:超类的方法可以被许多子类继承(从而重用)。然而,类层次结构的目的并不总是重用代码。有时,其目的是对某个领域中存在的各种可能性进行建模,例如图形库支持的形状类型或金融应用程序支持的贷款类型。当以这种方式使用类层次结构时,限制子类集可以简化建模。

例如,在图形库中,类的作者Shape可能希望只有特定的类可以扩展Shape,因为库的大部分工作涉及以适当的方式处理每种形状。作者感兴趣的是处理_已知_子类的代码的清晰度Shape,而不是编写代码来防御_未知_子类Shape。允许任意类扩展Shape,从而继承其代码以供重用,并不是这种情况下的目标。不幸的是,Java 假定代码重用始终是一个目标:如果Shape可以扩展,那么它就可以通过任意数量的类进行扩展。放松这个假设是有帮助的,这样作者就可以声明一个不能由任意类扩展的类层次结构。在这样一个封闭的类层次结构中,代码重用仍然是可能的,但超出了则不然。

Java 开发人员熟悉限制子类集的想法,因为它经常出现在 API 设计中。该语言在这方面提供了有限的工具:要么创建一个类final,因此它有零个子类,要么将一个类或其构造函数设为包私有,因此它只能在同一个包中拥有子类。JDK 中出现了包私有超类的示例:

package java.lang;

abstract class AbstractStringBuilder {...}
public final class StringBuffer extends AbstractStringBuilder {...}
public final class StringBuilder extends AbstractStringBuilder {...}

AbstractStringBuilder当目标是代码重用时,例如共享其代码的子类,包私有方法非常有用append。然而,当目标是建模替代方案时,该方法毫无用处,因为用户代码无法访问关键抽象(超类)以switch覆盖它。不可能允许用户访问超类而不允许他们扩展它。 (即使在声明及其子类的图形库中Shape,如果只有一个包可以访问Shape.)

总之,超类应该可以被广泛_访问_(因为它代表了用户的重要抽象),但不能广泛_扩展_(因为它的子类应该仅限于作者已知的子类)。这样的超类应该能够表达它是与一组给定的子类共同开发的,既可以为读者记录意图,也可以允许 Java 编译器执行。同时,超类不应过度约束其子类,例如强迫它们final或阻止它们定义自己的状态。

描述

_密封_类或接口只能由那些允许这样做的类和接口进行扩展或实现。

sealed通过将修饰符应用于其声明来密封类。然后,在任何extendsandimplements子句之后,该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 {...}

当允许的子类的大小和数量较小时,将它们声明在与密封类相同的源文件中可能会很方便。当以这种方式声明它们时,sealed类可以省略permits子句,Java编译器将从源文件中的声明推断出允许的子类(可能是辅助类或嵌套类)。例如,如果在 中找到以下代码Shape.java,则推断密封类Shape具有三个允许的子类:

package com.example.geometry;

abstract sealed class Shape {...}
... class Circle extends Shape {...}
... class Rectangle extends Shape {...}
... class Square extends Shape {...}

密封类的目的是让客户端代码清楚、结论性地推理_所有_允许的子类。推理子类的传统方法是使用测试链if,但分析此类链对于编译器来说很困难,因此无法确定测试是否涵盖了_所有_允许的子类。例如,以下方法将导致编译时错误,因为编译器不同意开发人员的信念,即 的每个子类都经过测试并导致语句:else``instanceof``Shape``return

int getCenter(Shape shape) {
if (shape instanceof Circle) {
return ... ((Circle)shape).center() ...
} else if (shape instanceof Rectangle) {
return ... ((Rectangle)shape).length() ...
} else if (shape instanceof Square) {
return ... ((Square)shape).side() ...
}
}

附加一个包罗万象的else条款将违背开发人员的信念,即测试已经是详尽无遗的。此外,如果开发人员的信念被证明是错误的,编译器也没有能力拯救他们。假设上面的代码被意外编辑以省略instanceof Rectangle测试;不会发生编译时错误。 (对于 3 个允许的子类,这种遗漏可能很容易被发现,但对于 10 个或 20 个子类则不然。即使只有 3 个,代码写起来也很令人沮丧,读起来也很乏味。)

在支持模式匹配的未来版本中将实现清晰、结论性地推理允许的子类的能力。客户端代码将能够使用_类型测试模式_(JEP 375)切换实例,而不是使用if-检查密封类的实例。这允许编译器检查模式是否_详尽_。例如,给定以下代码,编译器将推断覆盖了每个允许的子类,因此不需要子句(或其他总模式);此外,如果缺少这三种情况中的任何一种,编译器都会给出错误:else``Shape``default

int getCenter(Shape shape) {
return switch (shape) {
case Circle c -> ... c.center() ...
case Rectangle r -> ... r.length() ...
case Square s -> ... s.side() ...
};
}

密封类对其允许的子类(由其permits子句指定的类)施加三个约束:

  1. 密封类及其允许的子类必须属于同一模块,并且如果在未命名模块中声明,则必须属于同一包。

  2. 每个允许的子类必须直接扩展密封类。

  3. 每个允许的子类必须选择一个修饰符来描述它如何继续由其超类发起的密封:

  • 可以声明允许的子类final以防止其部分的类层次结构进一步扩展。
  • 允许的子类可以被声明sealed为允许其层次结构的一部分比其密封的超类所设想的进一步扩展,但以受限的方式。
  • 可以声明允许的子类,non-sealed以便其层次结构的一部分恢复为对未知子类的扩展开放。 (密封类不能阻止其允许的子类执行此操作。)

作为第三个约束的示例,Circle可以是finalwhile RectangleissealedSquareis non-sealed

package com.example.geometry;

public abstract sealed class Shape
permits Circle, Rectangle, Square {...}

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 non-sealed class Square extends Shape {...}

每个允许的子类必须使用一个且仅有一个修饰符finalsealed、 和。non-sealed一个类不可能既是sealed(隐含子类)又是final(隐含无子类),或者既是(non-sealed隐含子类)又是final(隐含无子类),又或者既是sealed(隐含受限制子类)又是non-sealed(隐含非限制子类)。

final修饰符可以被认为是一种强密封形式,完全禁止扩展/实现。也就是说,final在概念上等于sealed+ 一个permits不指定任何内容的子句;请注意,这样的permits子句不能用 Java 编写。)

_抽象类。_一个类,它是sealednon-sealed可能是abstract,并且有abstract成员。一个sealed类可以允许子类是abstract(前提是它们是 thensealednon-sealed,而不是final)。

_类可访问性。_由于extendsandpermits子句使用类名,因此允许的子类及其密封超类必须可以相互访问。然而,允许的子类彼此之间或密封类不需要具有相同的可访问性。特别是,子类可能比密封类更难访问;这意味着,在未来版本中,当交换机支持模式匹配时,某些用户将无法详尽地switch遍历子类,除非default使用子句(或其他总模式)。我们将鼓励 Java 编译器检测用户的信息是否switch没有用户想象的那么详尽,并自定义错误消息以推荐子句default

密封接口

与类的故事类似,通过将sealed修饰符应用于接口来密封接口。在任何extends指定超级接口的子句之后,用permits子句指定实现类和子接口。例如:

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 {...}

密封课程和记录

密封类与记录配合良好(JEP 384),这是 Java 15 的另一个预览功能。记录是隐式的final,因此带有记录的密封层次结构比上面的示例稍微简洁一些:

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 {...}

密封类和记录的组合有时被称为代数数据类型:记录允许我们表达_产品类型_,而密封类允许我们表达_求和类型_。

JDK 中的密封类

JDK 中如何使用密封类的一个示例位于为 JVM 实体java.lang.constant建模描述符的包中:

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 {...}

Java语法

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}

JVM 对密封类的支持

Java虚拟机sealed在运行时识别类和接口,并防止未经授权的子类和子接口进行扩展。

虽然是一个类修饰符,但结构中sealed没有标志。相反,密封类的文件有一个属性,隐式指示修饰符并显式指定允许的子类:ACC_SEALED``ClassFile``class``PermittedSubclasses``sealed

PermittedSubclasses_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 number_of_classes;
u2 classes[number_of_classes];
}

允许的子类列表是强制性的 - 即使编译器推断出允许的子类,这些推断的子类也会显式包含在属性中PermittedSubclasses

允许的子类的文件class不携带新属性。

当 JVM 尝试定义一个其超类或超接口具有PermittedSubclasses属性的类时,所定义的类必须由该属性命名。否则,IncompatibleClassChangeError将抛出一个。

反射API

public将添加以下方法java.lang.Class

  • java.lang.constant.ClassDesc[] getPermittedSubclasses()
  • boolean isSealed()

如果该类是密封的,该方法getPermittedSubclasses()将返回一个包含java.lang.constant.ClassDesc表示该类所有允许子类的对象的数组,如果该类未密封,则返回一个空数组。

isSealed如果给定的类或接口是密封的,则该方法返回 true。 (与之比较isEnum。)

备择方案

有些语言直接支持_代数数据类型_(ADT),例如 Haskell 的data功能。通过该功能的变体,可以更直接地以 Java 开发人员熟悉的方式表达 ADT enum,其中可以在单个声明中定义乘积之和。然而,这并不能支持所有所需的用例,例如求和范围涵盖多个编译单元中的类的用例,或者求和范围涵盖非产品的类的用例。

permits子句允许密封类(例如Shape前面所示的类)可由任何模块中的代码访问以进行调用,但只能由与密封类相同的模块(或相同的包,如果在未命名的模块)。这使得类型系统比访问控制系统更具表现力。仅使用访问控制,如果Shape可以通过任何模块中的代码访问调用(因为其包已导出),那么Shape也可以在任何模块中访问实现;如果Shape在任何其他模块中都不可访问以实现,则Shape在任何其他模块中也不可以访问为调用。

依赖关系

密封类不依赖于记录 (JEP 384) 或模式匹配 (JEP 375),但它们可以很好地与两者配合。