Java 密封类 sealed 有什么实际用途?
Java sealed 类通过 permits 在编译期限定子类,使 switch 模式匹配可穷尽检查、无需 default,并与 record 配合表达状态机、表达式树、支付结果等有限领域模型。
Java 密封类(sealed class,JDK 17 正式定稿,对应 JEP 409)最实际的价值只有一个:把「谁能继承我」从口头约定变成编译期规则,从而让 switch 模式匹配可以做到穷尽性检查、不再需要 default 兜底,同时让库作者能真正封死扩展点。
sealed 类和 final 类有什么区别?
结论:final 是「一个子类都不许有」,sealed 是「只许我点名的这几个子类有」,粒度更细。
一个 final 类完全不能继承,你没法在保留继承能力的同时限制继承者。sealed 解决的正是这个中间地带:类型本身是抽象的、有多个实现,但实现集合在写代码时就确定了。
标准写法是配合接口或抽象类:
public sealed interface Shape permits Circle, Square, Rectangle {}
如果所有子类都写在同一个源文件里,permits 可以省略,编译器会自动推断。
sealed 的子类必须遵守哪些规则?
结论:被 permits 列出的子类,必须显式声明自己是 final、sealed 或 non-sealed 三者之一,否则编译不过。
final:到此为止,不能再被继承。sealed:继续限制下一层继承者,要再写一个 permits。non-sealed:主动放弃限制,这一支恢复成普通开放继承。
另外两条硬性约束:如果父类在具名模块(module-info.java)里,permits 的子类必须和它同模块;如果在未命名模块里,必须同包。跨包跨模块的「远程继承」是被禁止的。
怎么让 switch 不再写 default?
结论:当 switch 的选择器类型是 sealed 类型时,编译器知道全部子类,只要每个子类都有对应分支,就是穷尽的,default 可以删掉。
double area = switch (shape) {
case Circle c -> Math.PI * c.r() * c.r();
case Square s -> s.side() * s.side();
case Rectangle r -> r.w() * r.h();
};
这段代码在 JDK 21(switch 模式匹配正式定稿,JEP 441)上合法。真正的收益不是少写一行 default,而是以后新增一个子类时,所有没处理它的 switch 会直接编译报错。这比运行时抛异常、或者悄悄走到 default 返回错误结果要可靠得多。
实际用途:用 sealed + record 表达领域模型
结论:sealed interface 加 record 是 Java 里表达「代数数据类型」最省事的组合,适合状态机、表达式树、支付结果、消息协议这类「就那么几种情况」的建模。
public sealed interface PaymentResult
permits Success, Failed, Pending {}
public record Success(String txId) implements PaymentResult {}
public record Failed(String code, String msg) implements PaymentResult {}
public record Pending(String txId, Instant expireAt) implements PaymentResult {}
对比用枚举:枚举的每个取值只有一个实例,无法携带不同的字段;sealed + record 的每个分支都是独立类型,可以带各自的数据,同时穷尽性检查和枚举一样强。
库作者怎么用它封住扩展点?
结论:把接口声明成 sealed,等于在公开 API 上写明「这个抽象只为我列出的实现服务」,外部代码无法再通过 implements 混进来。
典型场景是 SPI、插件接口、内部框架的回调接口。过去只能用注释写「请勿实现」,或者加一个包私有构造器绕弯;现在直接 sealed 一写,第三方编译就失败。运行时也能验证:Class.isSealed() 返回 true,getPermittedSubclasses() 拿到允许的子类数组。
用之前要确认的三件事
第一,编译和运行环境至少是 JDK 17,用 switch 模式匹配穷尽性检查则要 JDK 21。第二,sealed 限制的是继承关系,不是实例数量,别把它当枚举用。第三,一旦发布成 sealed,后续新增子类属于不兼容变更——所有下游的穷尽 switch 都会编译失败,这是特性也是代价,设计时要一次想清楚。
一句话收束:sealed 把「可能的子类集合」变成类型系统的一部分,让编译器替你守住穷尽性和扩展边界,这才是它区别于 final 和普通接口的实际用途。
转载请注明出处,版权归原作者所有。