提供"non-sealed"关键字会引发开发者的hack行为。 为什么我们允许一个"sealed"类被扩展为一个"non-sealed"类呢?
因为在现实世界的API中,有时我们希望支持特定的扩展点而限制其他扩展点。然而,Shape
的例子并不是特别生动形象,这也是为什么允许这种情况可能看起来很奇怪的原因之一。
密封类可以更好地控制谁可以扩展给定的可扩展类型。有几个理由可能会让您这样做,“确保没有人永远扩展层次结构”只是其中之一。
许多情况下,API具有几个“内置”抽象,然后是一个“逃生口”抽象;这使得API作者可以引导潜在的扩展者到为扩展而设计的逃生口。
例如,假设您有一个使用Command
模式的系统,其中有几个内置命令需要控制实现,并且一个UserPluginCommand
用于扩展:
sealed interface Command
permits LoginCommand, LogoutCommand, ShowProfileCommand, UserPluginCommand { ... }
// final implementations of built-in commands
non-sealed abstract class UserPluginCommand extends Command {
// plugin-specific API
}
这样的层次结构实现了两件事:
所有扩展都通过UserPluginCommand
进行过滤,它可以被设计为针对扩展进行防御并提供适合用户扩展的API,但我们仍然可以在设计中使用基于接口的多态性,因为完全不受控制的子类型不会出现;
系统仍然可以依靠四种允许的类型涵盖所有命令的实现。因此,内部代码可以使用模式匹配并确信其穷尽性:
switch (command) {
case LoginCommand(...): ... handle login ...;
case LogoutCommand(...): ... handle logout ...;
case ShowProfileCommand(...): ... handle query ...;
case UserPluginCommand uc:
// interact with plugin API
// no default needed, this switch is exhaustive
可能有无数种UserPluginCommand
的子类型,但系统仍然可以自信地推断它可以通过这四种情况来覆盖所有情况。
JDK中将利用此功能的API示例是java.lang.constant
,其中有两个设计用于扩展的子类型——动态常量和动态调用点。
我认为来自JEP 360的以下示例说明了它:
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 {...}
你想要只允许特定的类继承 Shape
。那么为什么要将 Square
设为 non-sealed
呢?因为你希望允许任何其他类继承 Square
(和它所在的层级)。
可以这样理解:任何想要继承 Shape
的类都必须使用 Circle
、Rectangle
或者 Square
这三个类之一来完成这个操作(中间有一个“是一个”关系)。因此,这个子层级的每个扩展类都是 Circle
、Rectangle
或者 Square
中的一个。
sealed
/non-sealed
的组合使你可以“封闭”你的层级的部分而不是全部(从根开始)。
请注意 JEP 360 对允许的类的定义:
每个允许的子类都必须选择一个修饰符来描述它如何继续由其超类启动的封闭:
这些选项包括:final
、sealed
或者 non-sealed
。你被迫明确说明,所以我们需要使用 non-sealed
来“打破”这个封印。
Brian Goetz 发布了一个现实中的用例,并解释了实际的好处。我想再举一个例子:
public sealed class Character permits Hero, Monster {}
public sealed class Hero extends Character permits Jack, Luci {}
public non-sealed class Monster extends Character {}
public final class Jack extends Hero {}
public final class Luci extends Hero {}
这个游戏有两个主要角色,还有几个敌人。主要角色已经确定,但是可以有许多不同的怪物。游戏中的每个角色都是英雄或怪物。
这只是一个最小的例子,希望更能说明问题,可能会发生变化,例如添加一个类CustomHero
,使修改器可以创建自定义英雄。
final
可以防止子类化,因此sealed
并不适用,但您在Rectangle
上指定了sealed
,这意味着它不是从Shape
继承而来。如果是这样,为什么您需要在Square
上使用non-sealed
?或者sealed
是继承的,您只是在Rectangle
上重复了它以进行确认,即使这是多余的? - Andreassealed
。 - AndreasSquare
没有 non-sealed
,那么编译器会报错。你必须明确指出。 - Jorn Verneesealed
类型的子类必须指定其中之一:final
、sealed
或 non-sealed
?在回答中说明这一点会很好,作为我们需要 non-sealed
关键字的原因之一。 - AndreasThrowable
最初只有两个子类 Error
和 Exception
,形成了两个基本类别。这两个类 Error
和 Exception
都允许有子类,并且应该有子类,但是直接从 Throwable
继承的子类不包括这两个子类以外的其他类,尽管在过去无法禁止它们。 - Holgernon-sealed
类允许打开继承层次结构的一部分,这意味着根密封类只允许一组封闭的子类来扩展它。public sealed class NumberSystem
// The permits clause has been omitted
// as all the subclasses exists in the same file.
{ }
final class Binary extends NumberSystem { .. }
final class Octal extends NumberSystem { .. }
final class HexaDecimal extends NumberSystem { .. }
non-sealed class Decimal extends NumberSystem { .. }
final class NonRecurringDecimal extends Decimal {..}
final class RecurringDecimal extends Decimal {..}
Shape
的类。如果你不想实例化它,你也可以将这个类定义为abstract
。你想要在一些类中扩展它,比如Circle
、Triangle
和Rectangle
。现在你已经实现了它,你想确保没有人能够扩展你的Shape
类。你能在不使用sealed
关键字的情况下做到吗?final
,这样你就无法将其扩展为任何子类。这就是sealed
关键字的作用!你可以将一个抽象类定义为sealed,并限制哪些类能够扩展它:public abstract sealed class Shape
permits Circle, Triangle, Rectangle {...}
记住,如果您的子类不在与Shape
类相同的包中,则必须在其名称前加上包名:
public abstract sealed class Shape
permits com.example.Circle { ... }
现在,当您声明这三个子类时,您必须使它们成为final
、sealed
或non-sealed
(应仅使用其中一个修饰符)。
现在,当您可以在所需的类中扩展Circle
?只有当您使用non-sealed
关键字告诉Java它可以被未知子类扩展时:
public non-sealed class Circle {...}
non-sealed
关键字的,它是除了sealed
关键字之外的另一个关键字。 - Jesper