← 返回题目列表

Sealed Class 是什么?permits、sealed、non-sealed 如何配合?

高频 中等 第 9 / 24 题 更新于 2026/07/26
Sealed ClassJava 17permits受限继承

简化版

Sealed Class 是 Java 17 正式加入的受限继承机制,sealed 类或接口只允许指定的直接子类型扩展或实现。每个获准的直接子类型必须明确选择 finalsealednon-sealed,分别表示终止继承、继续限制继承或重新开放继承。

详细版

父类型通常使用 permits 列出直接子类型;若允许的子类型与父类型在同一源文件中,编译器可以推断并省略 permits。命名模块中,允许的直接子类型必须与父类型位于同一模块;未命名模块中则必须位于同一包。

密封层次适合支付结果、语法树、领域命令等候选类型可控的模型,并可帮助模式 switch 做穷尽性检查。它限制的只是直接继承关系:non-sealed 子类下方可以再次出现任意后代,因此密封类型不是安全边界或权限控制机制。

完整版教学

一、为什么要限制继承集合

普通接口允许任意代码增加实现,调用方无法假设自己知道所有子类型。某些领域的候选集合本来就是有限的,例如支付结果只有成功、失败和处理中;把这种约束写进类型系统,编译器就能帮助维护模型。

public sealed interface PaymentResult
        permits Success, Failure, Pending {}

public record Success(String tradeNo) implements PaymentResult {}
public record Failure(String reason) implements PaymentResult {}
public final class Pending implements PaymentResult {}

Record 隐式为 final,因此可以直接作为获准的最终实现。

二、三种子类型选择

获准的直接子类型不能保持态度不明,必须声明以下修饰符之一:

  • final:到此为止,不能再产生子类。
  • sealed:仍允许继承,但继续用 permits 限制下一层直接子类型。
  • non-sealed:主动退出限制,这个分支重新变为开放继承。

non-sealed 只能用在 sealed 父类型的直接子类或子接口上,不能给任意普通类添加。父类型只控制下一层,后续是否封闭由每个分支继续决定。

三、位置约束与 permits 推断

在命名模块中,父类型与所有获准直接子类型必须属于同一模块,可以分布在不同包;在未命名模块中,它们必须在同一包。这个约束使编译器和 JVM 能可靠识别封闭层次,同时避免跨任意模块声明许可关系。

sealed interface Node {
    record NumberNode(int value) implements Node {}
    record AddNode(Node left, Node right) implements Node {}
}

直接子类型都声明在同一个源文件中时,可省略 permits,由编译器推断。大型模型显式列出 permits 往往更便于阅读。

四、与模式 switch 配合

当 switch 的选择器是密封类型,编译器可以根据允许的子类型判断分支是否完整:

static String describe(PaymentResult result) {
    return switch (result) {
        case Success s -> "成功:" + s.tradeNo();
        case Failure f -> "失败:" + f.reason();
        case Pending p -> "处理中";
    };
}

以后给密封层次增加新的直接子类型,原有穷尽 switch 可能在重新编译时失败,从而提醒维护者处理新情况。这比依赖一个宽泛的 default 分支更利于发现模型变化。

五、它不等于安全机制

Sealed Class 控制的是哪些类型能直接继承,并不限制谁能创建对象、调用方法、反射读取数据,也不能替代模块封装和权限校验。只要某条分支声明为 non-sealed,外部还可在满足普通 Java 可见性规则的地方继续扩展它。

选择 sealed 表示领域设计者确信候选集合应受控。插件接口、驱动接口等天然希望第三方扩展的 SPI,通常不应该密封。

六、用类型树看“只限制直接孩子”

sealed Payment permits Card, Cash
├─ final Card                  // 分支终止
└─ non-sealed Cash             // 从这里重新开放
   ├─ CouponCash
   └─ OtherCash

父类型的 permits 只列直接子类型,不需要也不能把所有孙类型都塞进去。若允许类型 Cash 选择 non-sealed,那么任意符合普通可见性规则的代码都可继续继承 Cash,switch 对 Payment 的穷尽分支只需处理 Cash 这一大类,但在 Cash 分支内部不能假设只有当前已知的两个后代。

子类型修饰符还能否扩展下一层是否受控适用意图
final分支结束完全封闭变体
sealed继续 permits分层受限模型
non-sealed重新开放刻意保留扩展点

若一个领域模型原有 3 个最终变体,新增第 4 个直接变体会让依赖穷尽 switch 的源码在重新编译时暴露遗漏。这是 sealed 的维护价值,但并不替代跨版本部署测试;调用方与模型 JAR 版本不一致时仍可能出现运行时匹配失败。

记忆钩子:sealed 管“谁有资格直接继承”,final/sealed/non-sealed 管“这个分支下一步关、控、开”。

七、常见误区与追问

  • 误区:permits 要列出所有层级的后代。 它只列允许的直接子类或直接子接口。
  • 误区:sealed 父类型的每个后代最终都必须封闭。 non-sealed 分支可以明确重新开放继承。
  • 误区:密封类型可以阻止未授权对象被创建或方法被调用。 它约束继承结构,不是鉴权、模块封装或安全沙箱。
  • 误区:同一源文件时仍必须写 permits。 直接子类型位于同一编译单元时可由编译器推断,但显式写出有时更易读。
  • 追问:命名模块和未命名模块的位置要求有什么区别? 命名模块要求同模块;未命名模块要求同包。
  • 追问:Record 为什么适合作为 sealed 的叶子? Record 隐式 final,天然表达一个封闭数据变体。
  • 追问:插件 SPI 适合 sealed 吗? 通常不适合,因为 SPI 的目的正是允许未知第三方实现。

八、加强记忆

sealed 父类型列出允许的直接孩子,孩子必须三选一:final 关闭、sealed 继续受控、non-sealed 重新开放。它最适合有限领域模型并能增强 switch 穷尽检查,但只约束继承层次,不提供权限安全。