JDK JDK 学习手册8 → 27 · JDK 专用学习站
QUICKREF · TYPE SYSTEM · JEP 409

sealed:封闭类型集合

JDK 17 · 速查主题 02 / 06 · 预计 20 分钟
核心20 分钟JEP 409
一句话sealed 把「只允许这几个实现」从注释变成编译器可检查的约束。它不改变多态,也不做访问控制;真正的收益是让类型集合在编译期闭合,从而支撑穷尽性检查。

学完你要能自己回答

  1. sealed 的 permits 有哪些写法?省略 permits 的条件是什么?
  2. 子类型为什么必须标 final、sealed 或 non-sealed?三者分别意味着什么?
  3. 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 适合「变体有限且由你控制」的领域模型:表达式、事件、状态机、协议消息。它不适合需要第三方实现的服务接口,也不适合每天新增变体的开放分类。

场景建议原因
编译器 / 解释器的 ASTsealed + record变体固定,穷尽性直接变成编译检查
订单状态机sealed + record状态转换需要覆盖所有状态
插件 SPI普通 interface实现方在外部,无法封闭
HTTP 错误码enum值集合固定,不需要携带不同形状的数据
需要携带不同字段的事件sealed interface + record 变体每个变体有自己的数据形状

sealed 与 enum 的区别:enum 的常量集合固定,每个常量共享同一个类;sealed 允许每个子类型有自己的字段与方法。需要「有限种形状,每种形状数据不同」时,sealed + record 更合适。

sealed 层级一旦公开,新增子类型就是二进制不兼容的变更:外部编译的穷尽 switch 可能不再覆盖新类型。把 sealed 类型当作公共 API 时,要在版本策略里写清楚。

版本边界BOUNDARY

这张表比 API 本身更值得记。写代码时先确认你的运行时版本。

特性 / API状态说明
sealed classes17 正式JEP 409;JDK 15 / 16 预览。
switch 类型模式17 预览JEP 406;需 --enable-preview,21 才正式(JEP 441)。
record pattern21 正式17 不支持,不能解构 record。
non-sealed17 正式重新打开继承,但这一支失去穷尽性保护。
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。
学习进度 记录保存在本机浏览器 · 不上传

JDK 学习手册 · JDK / Java 专用学习站 · 纯静态 · 双击 index.html 即可使用

本站专注 JDK 版本演进与 Java 教学,非 Oracle / OpenJDK 官方站点。版本数据来自 OpenJDK 官方 Features 表;课程示例已在对应本机 JDK 实测。构建日期 2026-11-12。