在Java语言中,类的继承体系长期以来要么完全开放,任何类都能继承非final父类;要么彻底封闭,用final禁止一切派生。这种非黑即白的控制力很难满足中间态需求:比如一个支付渠道基类,只允许支付宝、微信、银联三种实现,多一种都不行。Java 17正式引入的密封类(sealed class)机制,通过sealed修饰符与permits子句,让父类在声明时白名单式指定子类,从语言层面收紧继承边界。

sealed与permits的基础声明方式
要使用密封类,首先需要在class或interface前加上sealed关键字,并用permits显式列出允许继承或实现的子类名称。这些子类必须和密封类位于同一模块(如果是无名模块则同一包)内,且每个被许可的子类都必须明确选择自身的继承开放度:可以是final彻底封死、sealed继续向下约束,或non-sealed重新对外开放。
下面是一段典型的密封类定义,展示了父类如何限制子类范围。注意permits后面的类若写在别的文件,需保证包一致,且子类修饰符三选一,否则编译报错。
public abstract sealed class Payment permits Alipay, Wechat, UnionPay {
public abstract void pay(double amount);
}
public final class Alipay extends Payment {
public void pay(double amount) {
System.out.println("支付宝支付: " + amount);
}
}
public sealed class Wechat extends Payment permits WechatMini {
public void pay(double amount) {
System.out.println("微信支付: " + amount);
}
}
public non-sealed class UnionPay extends Payment {
public void pay(double amount) {
System.out.println("银联支付: " + amount);
}
}
public final class WechatMini extends Wechat {
// 继承自Wechat,因Wechat是sealed故此处合法
}
上述代码中,Payment作为密封抽象类,只允许三个直接子类。若有人新建一个class Paypal extends Payment,编译器会直接拒绝,因为Paypal不在permits列表中。这种限制在编译期生效,比运行期检查更早发现结构错误,也省去了手写校验逻辑。
需要强调的是,permits中的子类名不需要写全限定名,只用简单名即可,但它们必须是直接子类型,不能跨代。例如WechatMini没有直接出现在Payment的permits里,而是通过Wechat间接密封,这是合规的层级密封结构。
密封类与final、abstract的组合差异
很多开发者疑惑sealed和final到底什么关系。实际上final表示不能被任何类继承,而sealed表示只能被permits列出的类继承,二者是正交概念。一个类可以同时是abstract sealed,表示它是抽象且受限的父类;也可以是final class作为被许可的叶子节点。non-sealed则是反向操作,代表该类虽继承自密封类,但自己不再限制后代。
从设计意图看,abstract sealed常用于代数数据类型的建模。比如表达式树中,Expr密封后只允许Num、Add、Mul三种节点,配合switch模式匹配可以穷举,无需default分支。而过去用抽象类加包私有构造器模拟,不仅麻烦还容易被同包其他类钻空子。
public sealed interface Expr permits Num, Add, Mul {}
public final record Num(int val) implements Expr {}
public final record Add(Expr left, Expr right) implements Expr {}
public final record Mul(Expr left, Expr right) implements Expr {}
public static int eval(Expr e) {
return switch (e) {
case Num n -> n.val();
case Add a -> eval(a.left()) + eval(a.right());
case Mul m -> eval(m.left()) * eval(m.right());
};
}
上例利用sealed interface和record,构建出不可变且封闭的表达式模型。由于Expr只允许三种实现,switch在Java 21+下可穷尽匹配,编译器确认无遗漏。若以后误加一种子类却忘改switch,编译立即失败,安全性远高于普通继承。
另外,sealed不能和final同时修饰同一个类(语义冲突),也不能让permits列表为空(那等同于final,直接写final即可)。这些组合限制由编译器把关,写错会有清晰提示。
实际工程中的避坑与演进策略
在微服务API层,密封类适合用来定义稳定的返回体基类。例如后端规定Result只允许Success、Fail、Loading三种状态子类,前端根据类型做处理。若某天要加Timeout状态,必须改基类permits并重新编译,这倒逼开发者审视协议变更,避免悄悄扩展导致兼容问题。
一个常见坑是:把密封类放进模块A,却想让模块B的子类继承。由于permits要求同模块可见,跨模块需借助exports与opens,或者将基类设计为non-sealed桥接。此外,反射创建子类实例时,即使类在permits中,若构造器不可见也会失败,这和普通类一致。
// 错误示例:未写修饰符,默认final矛盾
// public sealed class Shape permits Circle {}
// 正确:Circle必须选final/sealed/non-sealed
public final class Circle extends Shape {
private final int r;
public Circle(int r) { this.r = r; }
}
在代码评审中,应检查所有sealed类的permits是否过多。如果列了十几种子类,说明抽象可能不够稳定,应考虑拆分配合non-sealed。密封类不是万能封装,滥用会让层次僵化。只在确实需约束扩展点时使用,才能兼顾灵活与安全。
最后注意,IDE如IntelliJ或javac版本低于17无法识别sealed语法。升级构建链后,配合@SuppressWarnings或模块化描述,可平滑落地。密封类作为Java类型系统的重要补强,用对地方能显著降低继承混乱带来的维护成本。