QUICKREF · TYPE SYSTEM · JEP 409
sealed:封闭类型集合
JDK 17 · 速查主题 02 / 06 · 预计 20 分钟
一句话sealed 把「只允许这几个实现」从注释变成编译器可检查的约束。它不改变多态,也不做访问控制;真正的收益是让类型集合在编译期闭合,从而支撑穷尽性检查。
学完你要能自己回答
- sealed 的 permits 有哪些写法?省略 permits 的条件是什么?
- 子类型为什么必须标 final、sealed 或 non-sealed?三者分别意味着什么?
- JDK 17 里 sealed 能直接让 switch 穷尽吗?还差哪一步?
封闭层级01
sealed 修饰类或接口,permits 列出直接子类型。直接子类型必须显式声明为 final、sealed 或 non-sealed,把「这一支是否还能继续扩展」写清楚。
public sealed interface Shape permits Circle, Square, Triangle {}
public record Circle(double r) implements Shape {}
public record Square(double side) implements Shape {}
public non-sealed class Triangle implements Shape {
final double base;
final double height;
public Triangle(double base, double height) {
this.base = base;
this.height = height;
}
}
final 关闭这一支,sealed 继续受控扩展,non-sealed 重新打开继承。三种修饰符必须选一个,编译器不会替你猜。
| 子类型修饰符 | 含义 | 对穷尽性的影响 |
|---|---|---|
| final | 不能再被继承 | 这一支的叶子类型确定 |
| sealed | 只能被 permits 中的类型继承 | 层级继续闭合,仍可穷尽 |
| non-sealed | 恢复普通继承 | 这一支不再受穷尽性保护 |
sealed 不是访问控制,也不是「禁止实例化」。它只约束继承关系;能不能 new,仍由构造器可见性决定。
permits 的写法与约束02
如果所有子类型都在同一个编译单元(同一个源文件)里,permits 可以省略,编译器根据同一文件中的直接子类型推断。跨文件时必须显式写出。
// 同一个源文件:可以省略 permits
sealed interface Expr {}
record Num(int value) implements Expr {}
record Add(Expr left, Expr right) implements Expr {}
- sealed 类型在命名模块中时,子类型必须与它在同一个模块。
- sealed 类型在未命名模块(classpath)中时,子类型必须与它在同一个包。
- 子类型不能是
final与sealed混写;也不能不写这三种修饰符。 - enum 的常量集合固定,可以实现 sealed 接口;record 天然 final,也可以作为 sealed 的叶子。
- 反射提供
Class.isSealed()与getPermittedSubclasses(),框架可据此做校验或代码生成。
System.out.println(Shape.class.isSealed());
for (Class<?> c : Shape.class.getPermittedSubclasses()) {
System.out.println(c.getSimpleName());
}
true Circle Square Triangle
permits 不是「允许谁访问」,而是「允许谁继承」。把第三方扩展点做成 sealed,会直接堵死外部实现;SPI 场景要谨慎。
穷尽性:sealed 只是一半03
在 JDK 17,sealed 本身不会让普通 switch 变穷尽。旧 switch 的 case 仍然只能是常量;要按子类型分支,需要 JEP 406 的 switch 类型模式,而它在 17 是预览。
static double area(Shape s) {
return switch (s) { // 17 预览:编译与运行都要 --enable-preview
case Circle c -> Math.PI * c.r() * c.r();
case Square q -> q.side() * q.side();
case Triangle t -> t.base * t.height / 2;
};
}
System.out.println(area(new Circle(1)));
System.out.println(area(new Square(2)));
3.141592653589793 4.0
这段代码没有 default,编译器仍能证明覆盖了所有 permitted 子类型。新增一个 Triangle 后,所有这样的 switch 会编译失败,迫使你补分支。这正是 sealed 的主要价值。
- 守卫条件不计入穷尽性:
case Circle c && c.r() > 0不能证明覆盖了 Circle。 - 17 预览的守卫语法是
&&;when是 JDK 19(JEP 427)才改的。 - 给 sealed switch 加
default会吞掉未来新增的子类型,编译期保护立即失效。 - 17 没有 record pattern,不能写
case Circle(double r);只能绑定类型后调访问器。
把「17 的 sealed」直接说成「switch 可以穷尽」是常见错误。准确说法是:sealed 提供封闭类型集合,穷尽性由 17 预览、21 正式的类型模式 switch 消费。
什么时候该用 sealed04
sealed 适合「变体有限且由你控制」的领域模型:表达式、事件、状态机、协议消息。它不适合需要第三方实现的服务接口,也不适合每天新增变体的开放分类。
| 场景 | 建议 | 原因 |
|---|---|---|
| 编译器 / 解释器的 AST | sealed + record | 变体固定,穷尽性直接变成编译检查 |
| 订单状态机 | sealed + record | 状态转换需要覆盖所有状态 |
| 插件 SPI | 普通 interface | 实现方在外部,无法封闭 |
| HTTP 错误码 | enum | 值集合固定,不需要携带不同形状的数据 |
| 需要携带不同字段的事件 | sealed interface + record 变体 | 每个变体有自己的数据形状 |
sealed 与 enum 的区别:enum 的常量集合固定,每个常量共享同一个类;sealed 允许每个子类型有自己的字段与方法。需要「有限种形状,每种形状数据不同」时,sealed + record 更合适。
sealed 层级一旦公开,新增子类型就是二进制不兼容的变更:外部编译的穷尽 switch 可能不再覆盖新类型。把 sealed 类型当作公共 API 时,要在版本策略里写清楚。
版本边界BOUNDARY
这张表比 API 本身更值得记。写代码时先确认你的运行时版本。
| 特性 / API | 状态 | 说明 |
|---|---|---|
| sealed classes | 17 正式 | JEP 409;JDK 15 / 16 预览。 |
| switch 类型模式 | 17 预览 | JEP 406;需 --enable-preview,21 才正式(JEP 441)。 |
| record pattern | 21 正式 | 17 不支持,不能解构 record。 |
| non-sealed | 17 正式 | 重新打开继承,但这一支失去穷尽性保护。 |
| sealed 子类型位置 | 17 约束 | 同模块;未命名模块中同包。 |
费曼自检SELF CHECK
先合上页面,用自己的话回答,再展开对照。能讲出因果和反例,才算真正掌握。
01. sealed 的子类型为什么必须写 final / sealed / non-sealed?
sealed 要保证类型集合在编译期可计算。每个直接子类型必须明确「关闭、继续封闭、重新开放」,否则编译器无法确定层级边界,也无法做穷尽性检查。
02. 什么情况下可以省略 permits?
当所有直接子类型都与 sealed 类型在同一个源文件中时,编译器可以推断并省略 permits;跨文件、跨类声明时必须显式列出。
03. JDK 17 的 sealed 能让 switch 不写 default 吗?
只有在使用 --enable-preview 开启 JEP 406 的 switch 类型模式时可以;普通 switch 仍然只接受常量,sealed 本身不改变 switch 规则。21 起类型模式正式,才成为稳定写法。
04. 为什么给 sealed switch 加 default 是危险的?
default 会接收未来新增的子类型,编译器不再提醒你补分支,穷尽性保护失效。只有确实要兼容未知实现时才加 default。
学习进度
记录保存在本机浏览器 · 不上传