密封类 Sealed Classes(JDK 15 预览 / JDK 17 正式)
飞翔科技的支付系统最近出了一个线上事故。孔蓝定义了一个
PaymentMethod接口,本意只让CreditCard、WeChatPay、Alipay三个类实现。但新来的实习生不知道这个约定,写了一个BitcoinPayment直接实现了PaymentMethod——结果支付网关不认识这个新类型,导致了未捕获的异常。"如果 Java 能让我们在代码层面限制谁能实现这个接口就好了。"孔蓝在事故复盘会上说。
白歌打开 JDK 17 的发布说明:"Sealed Classes,专门解决你这个痛点。用
sealed和permits关键字,白名单式地控制继承——不在名单上的类,编译器根本不让你编译通过。"
定义表
| 概念 | 描述 |
|---|---|
| 密封类(Sealed Class) | JDK 17 正式特性,使用 sealed 关键字限制哪些类可以继承或实现当前类/接口 |
| permits 子句 | 声明允许的子类/实现类的白名单,如 sealed class Shape permits Circle, Rectangle, Triangle |
| 子类声明修饰符 | 每个被 permits 的类必须声明为 final(终止继承)、sealed(继续控制继承)、non-sealed(开放继承)之一 |
| 穷尽性(Exhaustiveness) | 密封类 + Switch 表达式协同——编译器知道所有可能的子类型,强制全覆盖 |
| 代数数据类型(ADT) | 密封类 + Record 组合使用,将数据和操作建模为一组封闭的可穷举变体 |
密封类的继承控制模型
基础用法:限制接口的实现者
public class SealedBasic {
// === JDK 17:sealed interface 限制实现者 ===
sealed interface PaymentMethod
permits CreditCard, WeChatPay, Alipay {
String pay(double amount);
}
// 实现类一:final 终止继承
static final class CreditCard implements PaymentMethod {
@Override
public String pay(double amount) {
return "信用卡支付: ¥" + amount;
}
}
// 实现类二:non-sealed 开放继承
static non-sealed class WeChatPay implements PaymentMethod {
@Override
public String pay(double amount) {
return "微信支付: ¥" + amount;
}
}
// 实现类三:sealed 继续控制
static final class Alipay implements PaymentMethod {
@Override
public String pay(double amount) {
return "支付宝支付: ¥" + amount;
}
}
// ❌ 不在 permits 白名单中,编译错误
// static class BitcoinPay implements PaymentMethod {} // 编译错误
public static void main(String[] args) {
PaymentMethod[] methods = {
new CreditCard(),
new WeChatPay(),
new Alipay()
};
for (PaymentMethod method : methods) {
System.out.println(method.pay(99.99));
}
}
}
输出:
信用卡支付: ¥99.99
微信支付: ¥99.99
支付宝支付: ¥99.99
进阶用法:密封类 + Switch 穷尽性
密封类与 Switch 表达式是天然的搭档——编译器知道所有子类型,无需 default 分支即可覆盖所有情况。
public class SealedSwitch {
// === 代数数据类型:密封类 + Record ===
sealed interface Shape permits Circle, Rectangle, Triangle {}
record Circle(double radius) implements Shape {}
record Rectangle(double width, double height) implements Shape {}
record Triangle(double base, double height) implements Shape {}
// === Switch 表达式利用穷尽性:无需 default ===
static double area(Shape shape) {
return switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.width() * r.height();
case Triangle t -> 0.5 * t.base() * t.height();
// 编译器自动检查穷尽性——无需 default!
};
}
public static void main(String[] args) {
Shape[] shapes = {
new Circle(5),
new Rectangle(4, 6),
new Triangle(3, 8)
};
for (Shape shape : shapes) {
System.out.printf("%s 的面积 = %.2f%n", shape, area(shape));
}
}
}
输出:
Circle[radius=5.0] 的面积 = 78.54
Rectangle[width=4.0, height=6.0] 的面积 = 24.00
Triangle[base=3.0, height=8.0] 的面积 = 12.00
密封类的组合使用:飞翔科技订单系统
public class SealedOrderSystem {
// === 密封接口定义订单状态的有限集合 ===
sealed interface OrderStatus
permits Pending, Processing, Shipped, Delivered, Cancelled {}
record Pending(String orderId, String createdAt) implements OrderStatus {}
record Processing(String orderId, String processor, int progressPercent) implements OrderStatus {}
record Shipped(String orderId, String trackingNumber, String shippedAt) implements OrderStatus {}
record Delivered(String orderId, String deliveredAt, String signedBy) implements OrderStatus {}
record Cancelled(String orderId, String reason, String cancelledBy) implements OrderStatus {}
// === 利用穷尽性处理每个状态 ===
static String handleStatus(OrderStatus status) {
return switch (status) {
case Pending p -> "订单 %s 已创建,等待支付。创建时间:%s".formatted(p.orderId(), p.createdAt());
case Processing pp -> "订单 %s 处理中(%d%%),负责人:%s"
.formatted(pp.orderId(), pp.progressPercent(), pp.processor());
case Shipped s -> "订单 %s 已发货,快递单号:%s,发货时间:%s"
.formatted(s.orderId(), s.trackingNumber(), s.shippedAt());
case Delivered d -> "订单 %s 已签收,签收人:%s,签收时间:%s"
.formatted(d.orderId(), d.signedBy(), d.deliveredAt());
case Cancelled c -> "订单 %s 已取消,原因:%s,操作人:%s"
.formatted(c.orderId(), c.reason(), c.cancelledBy());
};
}
public static void main(String[] args) {
OrderStatus[] orders = {
new Pending("ORD-001", "2026-06-14 09:00"),
new Processing("ORD-001", "小崔", 60),
new Shipped("ORD-001", "SF1234567890", "2026-06-14 15:30"),
new Delivered("ORD-001", "2026-06-15 10:20", "大翔"),
new Cancelled("ORD-002", "客户主动取消", "Frank")
};
System.out.println("=== 飞翔科技订单追踪系统 ===\n");
for (OrderStatus order : orders) {
System.out.println(handleStatus(order));
}
}
}
输出:
=== 飞翔科技订单追踪系统 ===
订单 ORD-001 已创建,等待支付。创建时间:2026-06-14 09:00
订单 ORD-001 处理中(60%),负责人:小崔
订单 ORD-001 已发货,快递单号:SF1234567890,发货时间:2026-06-14 15:30
订单 ORD-001 已签收,签收人:大翔,签收时间:2026-06-15 10:20
订单 ORD-002 已取消,原因:客户主动取消,操作人:Frank
易错场景
子类必须在同一个模块或包内
public class SealedScopePitfall {
// 在同一个 .java 文件中,子类可以在同一个编译单元
sealed interface Animal permits Dog, Cat {}
static final class Dog implements Animal {}
static final class Cat implements Animal {}
// ✅ 同一文件内的子类 OK
public static void main(String[] args) {
Animal a1 = new Dog();
Animal a2 = new Cat();
System.out.println("a1 = " + a1.getClass().getSimpleName());
System.out.println("a2 = " + a2.getClass().getSimpleName());
}
}
输出:
a1 = Dog
a2 = Cat
如果密封类和允许的子类位于不同包中,需要将它们放在同一个**模块(module)**内(即同一个命名模块),否则编译报错。这是 JDK 对密封类的安全约束。
子类必须声明 final / sealed / non-sealed
public class SealedSubclassModifierPitfall {
sealed interface Vehicle permits Car {}
// ❌ 编译错误:Car 必须声明为 final / sealed / non-sealed
// static class Car implements Vehicle {} // 错误
// ✅ 正确:明确声明继承策略
static final class Car implements Vehicle {
String model = "飞翔科技专用车";
}
public static void main(String[] args) {
Car car = new Car();
System.out.println(car.model);
}
}
输出:
飞翔科技专用车
permits 子句中的类必须直接继承密封类
public class SealedDirectPermitPitfall {
sealed interface Shape permits Circle {}
static final class Circle implements Shape {}
// ❌ 间接实现者不能出现在 permits 中,也不需要
// Circle 已经是 final,不可能有子类间接实现 Shape
// static class SpecialCircle extends Circle {} // 编译错误:Circle 是 final
public static void main(String[] args) {
Shape s = new Circle();
System.out.println("形状: " + s.getClass().getSimpleName());
}
}
输出:
形状: Circle
面试考点
面试官常问的三个问题:
问题一:"sealed class 和 final class 有什么区别?什么时候用 sealed 而不是 final?"
答案:
final类完全终止继承——任何人都不能继承它。sealed类则允许指定的类继承它——是一种"受控的开放性"。当你的领域模型有一组固定的、已知的变体(如订单状态:待支付/处理中/已发货/已签收/已取消),用 sealed class 比 final class 更合适——你自己可以定义所有合法子类型,但外部代码无法随意扩展。密封类比 final 灵活(允许有限继承),比 non-sealed 安全(阻止任意扩展)。
问题二:"sealed class 和 Switch 表达式的穷尽性有什么关系?"
答案:密封类告诉编译器"所有可能的子类型都在 permits 列表中",因此 Switch 表达式在处理密封类型时可以自动验证穷尽性——不需要 default 分支。当你在 permits 列表中添加新的子类型,Switch 表达式会自动报编译错误,强制你处理新类型。这从根本上消灭了"新增枚举值但忘记更新 switch"这类 Bug。这是语言设计层面的深度协同。
问题三:"non-sealed 和普通类有什么区别?"
答案:
non-sealed是在密封体系中的"开放节点"——它自己是密封类 permits 白名单中的一员,但它把自己标记为"我的子类型不受限制",恢复了对该分支的正常继承开放性。语法等同于普通类,但多了一个显式意图声明:"我虽然是被密封类限制的,但我选择开放继承"。在语义上,non-sealed 类比普通类多了一层"我是密封体系的一部分"的含义。
白歌在架构文档中写道:"Sealed Classes + Records + Switch 穷尽性,三个特性单看各有价值,组合在一起才是真正的范式变革——它让 Java 从'一切皆对象'走向'数据即数据,状态即状态,类型即类型'。支付系统的重构证明了这一点:sealed 接口让非法支付类型在编译期就被拦截,线上再也没有出现过'未知支付方式'的异常。"