将密封类扩展为非密封类有什么意义?

问题描述

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

提供“非密封”关键字将邀请开发人员进行黑客入侵。 为什么我们允许将密封类扩展为非密封类?

解决方法

由于在现实世界的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,但是我们仍然可以在设计中使用基于接口的多态性,这完全可以理解不受控制的子类型将不会出现;

  • 系统仍然可以依靠以下事实:四种允许的类型涵盖Command的所有实现。因此,内部代码可以使用模式匹配,并对其完全性有信心:

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可能有数以百万计的子类型,但是系统仍然可以自信地说这四种情况都可以覆盖海滨。

java.lang.constant中,将在JDK中利用此API的一个示例,其中有两个为扩展设计的子类型-动态常量和动态调用项。

,

我认为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(是一种关系)。

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


请注意JEP 360关于允许的类的内容:

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

选项为:finalsealednon-sealed。您被迫坦率,因此我们需要non-sealed才能“打破传统”。


Brian Goetz有posted个现实的用例,并解释了现实生活中的好处。我想添加另一个示例:

想象一下,您正在开发一款包含英雄和怪物的游戏。一些类可能是这样的:

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类,使修改者可以创建自定义英雄。

,

假设您想在代码中编写类Shape。如果您不想实例化它,也可以使它成为abstract类。您想将其扩展到CircleTriangleRectangle之类的某些类中。现在已经实现了它,您想要确保没有人能够扩展您的Shape类。如果没有sealed关键字,您可以做到吗?
不,因为您必须将其设置为final,在这种情况下,您将无法对其进行扩展以包含任何子类。这就是sealed关键字的所在!您将一个抽象类密封起来,并限制哪些类可以对其进行扩展:

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

请记住,如果您的子类与Shape类不在同一个包中,则必须在包中提及它们的名称:

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

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

现在何时可以在所需的类中扩展Circle?仅当您告诉Java时,才可以使用non-sealed关键字将其扩展为未知子类:

public non-sealed class Circle {...}