我真的不明白为什么non-sealed
在 JEP 360/Java 15 中有一个关键字。对我来说,密封类的扩展应该只是 final 或密封类本身。
提供“非密封”关键字将邀请开发人员进行黑客攻击。为什么我们允许将密封类扩展到非密封类?
我真的不明白为什么non-sealed
在 JEP 360/Java 15 中有一个关键字。对我来说,密封类的扩展应该只是 final 或密封类本身。
提供“非密封”关键字将邀请开发人员进行黑客攻击。为什么我们允许将密封类扩展到非密封类?
因为在现实世界的 API 中,有时我们希望在限制其他扩展点的同时支持特定的扩展点。然而,这些Shape
例子并不是特别令人回味,这就是为什么允许它看起来很奇怪的原因。
密封类是关于更好地控制谁可以扩展给定的可扩展类型。您可能想要这样做有几个原因,“确保没有人扩展层次结构”只是其中之一。
在很多情况下,一个 API 有几个“内置”抽象,然后是一个“逃生舱”抽象;这允许 API 作者将潜在的扩展者引导到为扩展而设计的逃生舱口。
例如,假设您有一个使用该Command
模式的系统,有几个您想要控制其实现的内置命令,以及一个UserPluginCommand
for 扩展:
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
,但系统仍然可以自信地推断出它可以用这四种情况覆盖海滨。
将在 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
介于两者之间。因此,此子层次结构的每个扩展类都将是 a或 a Circle
( is-a 关系)。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
,使模组制作者能够创建自定义英雄。
想象一下,您想Shape
在代码中编写一个类。abstract
如果你不想实例化它,你也可以制作这个类。您想在某些类中扩展它,例如Circle
,Triangle
和Rectangle
. 现在您已经实现了它,您希望确保没有人能够扩展您的Shape
类。sealed
没有关键字你能做到吗?
不,因为你必须要么做到,final
在这种情况下你将无法扩展它以拥有任何子类。这就是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
所需的类?只有当你告诉 Java 它可以通过使用non-sealed
关键字被未知子类扩展时:
public non-sealed class Circle {...}
根据此文档,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 {..}