本文目录导读:

- 目录导读
- 密封类是什么?为什么需要它?
- 密封类的核心语法与约束条件
- 三个真实案例:从订单状态到图形计算
- 密封类 vs 抽象类 vs 枚举:选型对比
- 编译期与运行期的魔法:模式匹配配合
- 常见陷阱与性能优化建议
- FAQ 问答:开发者最关心的 8 个问题
Java 15密封类实战指南:从案例到最佳实践,彻底告别臃肿继承
目录导读
- 密封类是什么?为什么需要它?
- 密封类的核心语法与约束条件
- 三个真实案例:从订单状态到图形计算
- 密封类 vs 抽象类 vs 枚举:选型对比
- 编译期与运行期的魔法:模式匹配配合
- 常见陷阱与性能优化建议
- FAQ 问答:开发者最关心的 8 个问题
密封类是什么?为什么需要它?
在 Java 15 中,密封类(Sealed Class)作为预览特性正式亮相(JEP 360),并在 Java 17 中成为正式特性,它的核心目标是限制继承边界——既然抽象类允许无限子类化,导致代码混乱和脆弱性,那么密封类就明确告诉编译器:“我的子孙类只有这些,不许再多了。”
实际痛点:在没有密封类之前,如果你写了一个 Shape 抽象类,任何第三方库都可以继承它并添加一个 WeirdShape,导致 switch 语句必须写 default 分支来处理“未知类型”,密封类将这种“开放性”转为“封闭性”,极大提升了代码安全性与可读性。
密封类的核心语法与约束条件
public sealed class OrderStatus permits Pending, Paid, Shipped, Cancelled {
// 类体
}
关键约束:
- 使用
sealed关键字修饰。 - 必须通过
permits显式列出所有允许的子类(子类需同包或同模块)。 - 每个直接子类必须声明为
final、sealed或non-sealed(即非密封)。 - 密封类不能是接口,但接口也可以密封(
sealed interface)。
三个真实案例:从订单状态到图形计算
订单状态机(电商系统)
public sealed interface OrderState permits New, Paid, Completed, Cancelled {
double handle(Order order);
}
final class New implements OrderState {
public double handle(Order order) { return 0; }
}
final class Paid implements OrderState {
public double handle(Order order) { return order.total() * 0.1; } // 支付折扣
}
sealed class Cancelled implements OrderState permits Refunded { ... }
non-sealed class Refunded implements OrderState { ... }
优势:所有状态一目了然,编译器禁止未登记的异常状态。
图形面积计算(数学库)
public sealed abstract class Shape permits Circle, Square, Rectangle { }
final class Circle extends Shape {
final double radius;
Circle(double r) { this.radius = r; }
}
final class Square extends Shape {
final double side;
Square(double s) { this.side = s; }
}
final class Rectangle extends Shape {
final double w, h;
Rectangle(double w, double h) { this.w = w; this.h = h; }
}
配合 Java 16 的 instanceof 模式匹配,你可以写出无类型转换的干净 switch:
public static double area(Shape shape) {
if (shape instanceof Circle c) return c.radius * c.radius * Math.PI;
else if (shape instanceof Square s) return s.side * s.side;
else if (shape instanceof Rectangle r) return r.w * r.h;
else throw new IllegalStateException("Unexpected: " + shape);
}
JSON 解析器(XML 描述外部接口)
假设你需要处理不同的 JSON 节点类型:ObjectNode、ArrayNode、StringNode、NumberNode,密封类能确保解析器只处理这四种,不会因第三方新增节点导致解析崩溃。
密封类 vs 抽象类 vs 枚举:选型对比
| 维度 | 密封类 | 抽象类 | 枚举 |
|---|---|---|---|
| 子类限制 | 完全控制(permits) | 无限制 | 固定单例集合 |
| 状态行为 | 每个子类可携带状态 | 每个子类可携带状态 | 枚举常量可携带属性 |
| 扩展性 | 可扩展(但受控) | 任意扩展 | 不可扩展 |
| 适用场景 | 业务状态机、代数数据类型 | 多态继承体系 | 固定常量集合 |
核心建议:如果你需要“一组固定的可能性组合”,优先密封类;如果仅仅是常量,用枚举。
编译期与运行期的魔法:模式匹配配合
Java 15 预览版中,密封类与 switch 表达式搭配时,编译器可以检测 switch 是否穷尽。
String state = switch (orderState) {
case New -> "新订单";
case Paid -> "已支付";
case Completed -> "已完成";
case Cancelled -> "已取消";
// 无需 default,因为编译器知道只有这四种
};
如果遗漏了某个子类,编译直接报错,这比传统的 default 更加安全。
常见陷阱与性能优化建议
陷阱 1:子类必须与密封类位于同一个包或模块中,否则报错。
陷阱 2:密封类不能直接实例化(抽象或接口语义)。
陷阱 3:non-sealed 子类会“破防”——它允许不限数量的子类,使用时需谨慎。
陷阱 4:当密封类与记录(Record)结合时,自动实现的 equals/hashCode 会基于组件,可能造成隐式耦合。
性能建议:密封类本身无性能开销,但合理使用 switch 表达式比 if-else 链更适合 JIT 优化,对于频繁调用的面积计算,推荐使用 switch 模式匹配 + final 字段,避免重复提取字段。
FAQ 问答:开发者最关心的 8 个问题
Q1:密封类只能在 Java 17 使用吗?
A:Java 15 是预览特性,Java 17 转正,Java 21 LTS 完全支持,若使用低版本,需开启 --enable-preview。
Q2:permits 省略会怎样?
A:如果省略 permits,则默认允许同一编译单元内的所有直接子类(即同一个文件里的类)。
Q3:密封类可以有自己的构造函数吗?
A:可以,但构造函数必须是 protected 或 private,因为外部无法访问子类构造。
Q4:密封类能实现接口吗? A:可以,密封类可以同时实现接口,子类也必须实现该接口。
Q5:为什么不用 enum 来代替密封类?
A:枚举实例是单例,而密封类允许每个子类有多个实例(如多个 Circle 对象),并且支持复杂继承逻辑。
Q6:密封类与记录的配合推荐吗? A:非常推荐!记录(Record)是不可变数据的压缩表达,密封类限定种类,两者组合是完美的代数数据类型实现。
Q7:如果第三方子类需要继承,怎么办?
A:你可以将该第三方子类声明为 non-sealed,但这会打开无限扩展的“后门”,建议在模块边界内严格设计。
Q8:密封类会影响序列化或 Jackson 吗?
A:Jackson 2.13+ 支持密封类的多态反序列化,你需要配置 @JsonTypeInfo 和 @JsonSubTypes,密封类只是约束继承,不改变序列化机制。
密封类不是银弹,但它是 Java 向“声明式、穷尽式”编程推进的关键一步,从今天开始,在你下一个状态机或规则引擎中尝试使用它,你会发现代码的对抗性大幅降低,重构时更加从容。限制继承,就是保护未来。