将一个密封类与一个非密封类进行扩展的意义何在?

55
我真的不太明白为什么在JEP-360 / Java 15中会有一个"non-sealed"关键字。对我来说,一个"sealed"类的扩展应该只能是"final"或者是一个"sealed"类本身。
提供"non-sealed"关键字会引发开发者的hack行为。 为什么我们允许一个"sealed"类被扩展为一个"non-sealed"类呢?

3
@Ayox,请查看此链接:https://openjdk.java.net/jeps/360 中的“Motivation”部分。 - Omid.N
已经阅读过了(否则我不会引用它),但是我没有理解,所以我才在问。 - Ayox
3
如果提供“非密封的”关键字,将会邀请开发人员进行黑客攻击。- 你为什么认为这是不好的?你在谈论哪些“黑客攻击”? - Jorn Vernee
4个回答

82

因为在现实世界的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,其中有两个设计用于扩展的子类型——动态常量和动态调用点。


24

我认为来自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 的类都必须使用 CircleRectangle 或者 Square 这三个类之一来完成这个操作(中间有一个“是一个”关系)。因此,这个子层级的每个扩展类都是 CircleRectangle 或者 Square 中的一个。

sealed/non-sealed 的组合使你可以“封闭”你的层级的部分而不是全部(从根开始)。


请注意 JEP 360 对允许的类的定义:

每个允许的子类都必须选择一个修饰符来描述它如何继续由其超类启动的封闭:

这些选项包括:finalsealed 或者 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,使修改器可以创建自定义英雄。


1
太好了。现在让我们定个规矩,始终添加一个更多的派生允许类名为“其他”,以防我们改变这个愚蠢的密封决定。 - Gonen I
非常好的例子,但是由于我自己没有阅读过JEP 360,请为我澄清一下:我知道final可以防止子类化,因此sealed并不适用,但您在Rectangle上指定了sealed,这意味着它不是从Shape继承而来。如果是这样,为什么您需要在Square上使用non-sealed?或者sealed是继承的,您只是在Rectangle上重复了它以进行确认,即使这是多余的? - Andreas
1
如果你这样做,那么一开始就不要使用 sealed - Andreas
1
@Andreas 如果 Square 没有 non-sealed,那么编译器会报错。你必须明确指出。 - Jorn Vernee
那么 sealed 类型的子类必须指定其中之一:finalsealednon-sealed?在回答中说明这一点会很好,作为我们需要 non-sealed 关键字的原因之一。 - Andreas
6
一个更好的现实生活例子是:Throwable 最初只有两个子类 ErrorException,形成了两个基本类别。这两个类 ErrorException 都允许有子类,并且应该有子类,但是直接从 Throwable 继承的子类不包括这两个子类以外的其他类,尽管在过去无法禁止它们。 - Holger

1
根据文档non-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 {..}

0
假设你想在你的代码中编写一个名为Shape的类。如果你不想实例化它,你也可以将这个类定义为abstract。你想要在一些类中扩展它,比如CircleTriangleRectangle。现在你已经实现了它,你想确保没有人能够扩展你的Shape类。你能在不使用sealed关键字的情况下做到吗?
不行,因为你必须将其定义为final,这样你就无法将其扩展为任何子类。这就是sealed关键字的作用!你可以将一个抽象类定义为sealed,并限制哪些类能够扩展它:
public abstract sealed class Shape 
    permits Circle, Triangle, Rectangle {...}

记住,如果您的子类不在与Shape类相同的包中,则必须在其名称前加上包名:

public abstract sealed class Shape
    permits com.example.Circle    { ... }

现在,当您声明这三个子类时,您必须使它们成为finalsealednon-sealed(应仅使用其中一个修饰符)

现在,当您可以在所需的类中扩展Circle?只有当您使用non-sealed关键字告诉Java它可以被未知子类扩展时:

public non-sealed class Circle {...} 

2
这并没有回答问题。问题是关于non-sealed关键字的,它是除了sealed关键字之外的另一个关键字。 - Jesper
2
哇,这个功能似乎直接违反了开闭原则。我预见到许多情况下你会得到错误的许可列表,并不得不在未来重新打开它。 - Gonen I
@ Jesper 我编辑了我的答案!我希望你已经明白了这个新功能的目的! - Omid.N

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接