扩展一个密封类与非密封类有什么意义呢?

huangapple go评论66阅读模式
英文:

What is the point of extending a sealed class with a non-sealed class?

问题

我真的不太理解为什么在 JEP 360/Java 15 中会有一个 non-sealed 关键字。
对我来说,密封类的扩展只应该是 final,或者是一个密封类本身。

提供这个 "non-sealed" 关键字会引导开发者使用一些非正规的手段。
为什么我们要允许一个密封类被扩展为一个非密封类呢?

英文:

I don't really understand why there is a non-sealed keyword in JEP 360/Java 15.
For me, an extension of a sealed class should only be final or a sealed class itself.

Providing the "non-sealed" keyword will invite the developer for hacks.
Why are we allowing a sealed class to be extended to a non-sealed class?

答案1

得分: 76

因为在现实世界的API中,有时我们希望在限制其他情况的同时支持特定的扩展点。尽管“Shape”的示例并不特别具有启发性,这就是为什么允许这样做可能看起来有些奇怪。

密封类的作用是更精细地控制谁可以扩展给定的可扩展类型。有几个原因可能会让你这样做,“确保没有人会扩展层次结构”只是其中之一。

许多情况下,API会有几个“内置”的抽象,然后是一个“逃逸口”抽象;这允许API作者引导潜在的扩展者使用为扩展而设计的逃逸口。

例如,假设你有一个使用Command模式的系统,有一些内置命令,你想要控制其实现,还有一个用于扩展的UserPluginCommand

sealed interface Command
    permits LoginCommand, LogoutCommand, ShowProfileCommand, UserPluginCommand { ... }

// 内置命令的最终实现

non-sealed abstract class UserPluginCommand extends Command {
    // 插件特定的API
}

这样的层次结构实现了两个目标:

  • 所有扩展都通过UserPluginCommand进行,它可以为扩展进行防御性设计,并提供适合用户扩展的API,但我们仍然可以在设计中使用基于接口的多态性,因为我们知道完全不受控制的子类型将不会出现;

  • 系统仍然可以依赖于这四种允许的类型涵盖了所有Command的实现。因此,内部代码可以使用模式匹配,并对其穷尽性感到自信:

switch (command) {
    case LoginCommand(...): ... 处理登录 ...;
    case LogoutCommand(...): ... 处理注销 ...;
    case ShowProfileCommand(...): ... 处理查询 ...;
    case UserPluginCommand uc: 
        // 与插件API交互
    // 不需要默认情况,此switch是穷尽的
}

可能会有无数个UserPluginCommand的子类型,但系统仍然可以自信地推断它可以通过这四种情况来涵盖所有可能性。

在JDK中会利用这一点的一个API示例是java.lang.constant,其中有两个为扩展而设计的子类型--动态常量和动态调用站点。

英文:

Because in real-world APIs, sometimes we want to support specific extension points while restricting others. The Shape examples are not particularly evocative, though, which is why it might seem an odd thing to allow.

Sealed classes are about having finer control over who can extend a given extensible type. There are several reasons you might want to do this, and "ensuring that no one extends the hierarchy ever" is only one of them.

There are many cases where an API has several "built in" abstractions and then an "escape hatch" abstraction; this allows API authors to guide would-be extenders to the escape hatches that are designed for extension.

For example, suppose you have a system using the Command pattern, there are several built-in commands for which you want to control the implementation, and a UserPluginCommand for extension:

sealed interface Command
    permits LoginCommand, LogoutCommand, ShowProfileCommand, UserPluginCommand { ... }

// final implementations of built-in commands

non-sealed abstract class UserPluginCommand extends Command {
    // plugin-specific API
}

Such a hierarchy accomplishes two things:

  • All extension is funneled through the UserPluginCommand, which can be designed defensively for extension and provide an API suited to user extension, but we can still use interface-based polymorphism in our design, knowing that completely uncontrolled subtypes will not appear;

  • The system can still rely on the fact that the four permitted types cover all implementations of Command. So internal code can use pattern matching and be confident in its exhaustiveness:

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

There may be a zillion subtypes of UserPluginCommand, but the system can still confidently reason that it can cover the waterfront with these four cases.

An example of an API that will take advantage of this in the JDK is java.lang.constant, where there are two subtypes designed for extension -- dynamic constants and dynamic callsites.

答案2

得分: 23

我认为来自 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 的类都必须在 CircleRectangleSquare 中进行扩展,所以这个子继承体系的每个扩展类都将是 CircleRectangleSquare(is-a 关系)。

sealed/non-sealed 的组合允许您仅封装层次结构的部分部分,而不是全部(从根部开始)。


请注意 JEP 360 告诉我们有关允许的类的内容:

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

选项有:finalsealednon-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 {}

游戏中有两个主要角色以及多个敌人。主要角色已经确定,但是可以有任意数量的不同怪物。游戏中的每个角色都是英雄或怪物。

这是一个最小化的示例,希望更具说明性。可能会有变化,例如添加一个允许 modder 创建自定义英雄的 CustomHero 类。

英文:

I think the following example from the JEP 360 is showing it:

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

You want to permit only the specified classes to extend Shape. Now what's the point in making Square non-sealed? Because you want to allow any other class to extend Square (and the hierarchy).

Think of it like that: Any class that wants to extend Shape will have to do that with either Circle, Rectangle or Square in between. So every extending class of this sub-hierarchy will be either a Circle, Rectangle or a Square (is-a relationship).

The sealed/non-sealed-combination allows you to "seal" only parts of your hierarchy, not all of it (starting from a root).


Notice what the JEP 360 tells us about the permitted classes:

> Every permitted subclass must choose a modifier to describe how it continues the sealing initiated by its superclass:

The options are: final, sealed or non-sealed. You are forced to be explicit, so we need non-sealed to "break the seal".


Brian Goetz has posted a realistic use case and explained what the real life benefits are. I want to add another example:

Imagine you are developing a game with heroes and monsters. Some classes could be like that:

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

The game has two main characters and there are several enemies. The main characters are set in stone but there can be as many different monsters as you like. Every character in the game is either a hero or a monster.

This is a minimal example, which is hopefully a little more illustrative, and there might be changes, e.g. the addition of a class CustomHero that enables modders to create custom heroes.

答案3

得分: 1

根据这份文档non-sealed 类允许将继承层次结构的一部分开放给外部。这意味着根 sealed 类只允许一组封闭的子类来扩展它。

然而,这些子类仍然可以通过使用 non-sealed 关键字允许任意数量的子类来扩展自己。

public sealed class NumberSystem
    // 省略了 permits 子句
    // 因为所有的子类都在同一个文件中。
{ }
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 {..}
英文:

According to this documentation, the non-sealed class allows opening part of the inheritance hierarchy to the world. It means the root sealed class permits only a closed set of subclasses to extend it.

However, the subclasses can still allow themselves to be extended by any number of subclasses by using the non-sealed keyword.

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

答案4

得分: 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 {...} 
英文:

Imagin you wanna write a class Shape in your code. You can make this class abstract too if you don't wanna instantiate it. You want to extend it in some classes like Circle , Triangle, and Rectangle. Now that you have implemented it you want to make sure that no one will be able to extend your Shape class. Can you do it without the sealed keyword? <br>
No, because you will have to either make it final in which case you won't be able to extend it to have any subclasses. This is where the sealed keyword comes in! You make an abstract class sealed and restrict that which classes will be able to extend it:

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

Remember if your subclasses are not in the same package as Shape class, you will have to mention their name with packages:

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

Now, when you are declaring those three subclasses you have to make them final, sealed or non-sealed (One and only one of these modifiers should be used).<br>

Now when you can extend Circle in desired classes? Only when you tell Java it can be extended by unknown subclasses by using non-sealed keyword:

public non-sealed class Circle {...} 

huangapple
  • 本文由 发表于 2020年9月12日 20:03:20
  • 转载请务必保留本文链接:https://go.coder-hub.com/63860110.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定