JDK17 Sealed Classes受限类:受控继承的设计哲学
2026/4/30大约 11 分钟
JDK17 Sealed Classes受限类:受控继承的设计哲学
适读人群:对面向对象设计有追求、想理解代数数据类型的Java开发者 | 阅读时长:约16分钟
开篇故事
2022年,我在做一个订单系统的重构,订单状态有7种:待支付、已支付、已发货、已确认收货、申请退款、已退款、已关闭。
原来的代码里,OrderStatus是个枚举,每种状态有些特有数据(比如"已退款"状态需要记录退款金额和退款时间)。但枚举放不了复杂数据,于是有人在枚举旁边加了个OrderStatusDetail类,里面装所有状态的所有字段,大部分字段在大多数状态下都是null。
另一种方案是做继承体系:OrderStatus抽象类,七个子类。但这个设计有个问题:OrderStatus对外是开放的,任何人都可以再写一个第八种状态继承它,但系统根本没准备好处理第八种状态——结果可能是运行时异常。
JDK17的Sealed Classes完美解决了这个问题:允许继承,但控制谁可以继承。
一、Sealed Classes的设计动机
1.1 继承的两难困境
传统Java的类层次有两个极端:
- final类:完全禁止继承——安全,但失去了多态的灵活性
- 普通类:允许任意继承——灵活,但失去了对继承体系的控制
没有中间地带。
1.2 代数数据类型(ADT)
在函数式语言(Haskell, Scala, Rust)中,这类问题用"代数数据类型"解决:
-- Haskell:一个类型只能是这几种情况之一
data OrderStatus
= PendingPayment
| Paid PaymentInfo
| Shipped ShipmentInfo
| Completed CompletionInfo
| RefundRequested RefundInfo
| Refunded RefundInfo Amount
| Closed String -- 关闭原因编译器知道OrderStatus只有这几种情况,模式匹配时如果漏掉一种,编译器会警告。Java的Sealed Classes把这个概念带进了Java。
1.3 版本时间线
引入版本:JDK15(Preview,JEP 360)
第二次Preview:JDK16(JEP 397)
正式GA:JDK17(2021年9月,JEP 409)二、Sealed Classes深度解析
2.1 基本语法
// sealed类/接口:声明允许的子类/实现类
public sealed interface Shape
permits Circle, Rectangle, Triangle {}
// permits中的每个类必须:
// 1. 直接扩展/实现密封类型
// 2. 必须是以下三种之一:
// - final:不可再被继承
// - sealed:可以被继承,但继续控制
// - non-sealed:开放继承(显式声明放弃控制)2.2 三种子类型选择
sealed类的子类必须明确声明其继承策略:
1. final(终止继承):
final class Circle implements Shape { ... }
→ Circle不可被继承,继承链到此为止
2. sealed(继续控制):
sealed class ColoredShape implements Shape
permits RedShape, BlueShape { ... }
→ ColoredShape可被继承,但只能被RedShape和BlueShape继承
3. non-sealed(开放继承):
non-sealed class CustomShape implements Shape { ... }
→ CustomShape可以被任意类继承(相当于普通类)
→ 这是"有意为之"的逃生门,允许第三方扩展2.3 Sealed Classes与Pattern Matching的结合
Sealed Classes最重要的价值之一是让编译器能够进行穷举性检查:
sealed interface Expr permits Num, Add, Mul {}
switch (expr) {
case Num n -> ...;
case Add a -> ...;
// 如果漏掉Mul,编译器会报错:switch表达式未覆盖所有情况
// 这就是"穷举性"
}Mermaid图(Sealed Classes的层次结构):
三、完整代码示例
3.1 订单状态建模:旧写法vs新写法
import java.time.*;
import java.util.*;
/**
* Sealed Classes完整示例:订单状态建模
* 引入版本:JDK15 Preview;GA版本:JDK17(2021年9月,JEP 409)
*/
// ===== 旧写法1:枚举(无法携带不同数据)=====
enum OrderStatusEnum {
PENDING_PAYMENT, PAID, SHIPPED, COMPLETED, REFUND_REQUESTED, REFUNDED, CLOSED;
// 所有状态特有数据都需要放在外面,或者用一个大的数据类
}
// ===== 旧写法2:抽象类(无法控制继承)=====
// abstract class OrderStatus { } // 开放继承,任何人都能加第八种状态
// ===== 新写法:Sealed Classes =====
public sealed interface OrderStatus
permits OrderStatus.PendingPayment,
OrderStatus.Paid,
OrderStatus.Shipped,
OrderStatus.Completed,
OrderStatus.RefundRequested,
OrderStatus.Refunded,
OrderStatus.Closed {
// 每种状态的Record子类(携带特有数据)
// 待支付:有过期时间
record PendingPayment(Instant expireAt, double totalAmount) implements OrderStatus {}
// 已支付:有支付信息
record Paid(String transactionId, double paidAmount, Instant paidAt) implements OrderStatus {}
// 已发货:有物流信息
record Shipped(String trackingNumber, String carrier, Instant shippedAt) implements OrderStatus {}
// 已完成:确认收货
record Completed(Instant completedAt) implements OrderStatus {}
// 申请退款:有退款原因
record RefundRequested(String reason, Instant requestedAt) implements OrderStatus {}
// 已退款:有退款金额
record Refunded(double refundAmount, String refundTransactionId, Instant refundedAt) implements OrderStatus {}
// 已关闭:有关闭原因
record Closed(String reason, Instant closedAt) implements OrderStatus {}
}3.2 结合Switch Pattern Matching的状态机处理
/**
* 使用Sealed Classes + Switch Pattern Matching处理订单状态
*/
public class OrderStatusProcessor {
// 获取状态描述
static String describe(OrderStatus status) {
return switch (status) {
case OrderStatus.PendingPayment p ->
String.format("等待支付 %.2f 元,过期时间: %s", p.totalAmount(), p.expireAt());
case OrderStatus.Paid p ->
String.format("已支付 %.2f 元,流水号: %s", p.paidAmount(), p.transactionId());
case OrderStatus.Shipped s ->
String.format("已发货,承运商: %s,快递单号: %s", s.carrier(), s.trackingNumber());
case OrderStatus.Completed c ->
String.format("已完成,完成时间: %s", c.completedAt());
case OrderStatus.RefundRequested r ->
String.format("退款申请中,原因: %s", r.reason());
case OrderStatus.Refunded r ->
String.format("已退款 %.2f 元,退款流水: %s", r.refundAmount(), r.refundTransactionId());
case OrderStatus.Closed c ->
String.format("已关闭,原因: %s", c.reason());
// 注意:不需要default!因为sealed类的所有情况都被covers了
// 如果以后加了新的状态,这里会编译报错,强制处理新状态
};
}
// 判断是否可以申请退款
static boolean canRefund(OrderStatus status) {
return switch (status) {
case OrderStatus.Paid p -> true; // 已支付可以退款
case OrderStatus.Shipped s -> true; // 在途中也可以
case OrderStatus.Completed c ->
// 完成后7天内可退款
Duration.between(c.completedAt(), Instant.now()).toDays() <= 7;
case OrderStatus.PendingPayment p,
OrderStatus.RefundRequested r,
OrderStatus.Refunded r2,
OrderStatus.Closed c -> false; // 其他状态不能退款
};
}
// 状态流转合法性检查
static boolean canTransition(OrderStatus from, OrderStatus to) {
return switch (from) {
case OrderStatus.PendingPayment p -> to instanceof OrderStatus.Paid || to instanceof OrderStatus.Closed;
case OrderStatus.Paid p -> to instanceof OrderStatus.Shipped || to instanceof OrderStatus.RefundRequested;
case OrderStatus.Shipped s -> to instanceof OrderStatus.Completed || to instanceof OrderStatus.RefundRequested;
case OrderStatus.Completed c -> to instanceof OrderStatus.RefundRequested;
case OrderStatus.RefundRequested r -> to instanceof OrderStatus.Refunded || to instanceof OrderStatus.Paid;
case OrderStatus.Refunded r -> false; // 终态
case OrderStatus.Closed c -> false; // 终态
};
}
// 提取关键信息(带guard条件)
static Optional<String> extractTransactionId(OrderStatus status) {
return switch (status) {
case OrderStatus.Paid p -> Optional.of(p.transactionId());
case OrderStatus.Refunded r when r.refundAmount() > 0 ->
Optional.of(r.refundTransactionId());
default -> Optional.empty();
};
}
public static void main(String[] args) {
var now = Instant.now();
OrderStatus[] statuses = {
new OrderStatus.PendingPayment(now.plusSeconds(1800), 299.0),
new OrderStatus.Paid("TXN20240115001", 299.0, now),
new OrderStatus.Shipped("SF1234567890", "顺丰速运", now),
new OrderStatus.Completed(now.minusSeconds(86400 * 3)),
new OrderStatus.RefundRequested("商品损坏", now),
new OrderStatus.Refunded(299.0, "REF20240115001", now),
new OrderStatus.Closed("超时未支付", now)
};
for (var status : statuses) {
System.out.println("状态: " + describe(status));
System.out.println("可退款: " + canRefund(status));
System.out.println();
}
// 测试状态流转
var pending = new OrderStatus.PendingPayment(now.plusSeconds(1800), 99.0);
var paid = new OrderStatus.Paid("TXN001", 99.0, now);
var closed = new OrderStatus.Closed("取消", now);
System.out.println("待支付->已支付: " + canTransition(pending, paid)); // true
System.out.println("待支付->已关闭: " + canTransition(pending, closed)); // true
System.out.println("已退款->其他: " + canTransition(new OrderStatus.Refunded(99.0, "R001", now), paid)); // false
}
}3.3 表达式/AST建模(编译器/解释器场景)
/**
* 使用Sealed Classes建模JSON类型系统
* 体现"代数数据类型"的思想
*/
public sealed interface JsonValue
permits JsonNull, JsonBool, JsonNumber, JsonString, JsonArray, JsonObject {}
record JsonNull() implements JsonValue {
public static final JsonNull INSTANCE = new JsonNull();
}
record JsonBool(boolean value) implements JsonValue {
public static final JsonBool TRUE = new JsonBool(true);
public static final JsonBool FALSE = new JsonBool(false);
}
record JsonNumber(double value) implements JsonValue {}
record JsonString(String value) implements JsonValue {}
record JsonArray(List<JsonValue> elements) implements JsonValue {
public JsonArray {
elements = List.copyOf(elements); // 防御性拷贝
}
}
record JsonObject(Map<String, JsonValue> fields) implements JsonValue {
public JsonObject {
fields = Map.copyOf(fields);
}
}
class JsonSerializer {
// ===== 旧写法(没有sealed)=====
static String serializeOld(Object value) {
if (value == null) return "null";
if (value instanceof Boolean b) return b.toString();
if (value instanceof Number n) return n.toString();
if (value instanceof String s) return "\"" + escapeString(s) + "\"";
if (value instanceof List<?> list) {
var sb = new StringBuilder("[");
for (int i = 0; i < list.size(); i++) {
if (i > 0) sb.append(",");
sb.append(serializeOld(list.get(i)));
}
return sb.append("]").toString();
}
// ... 很容易漏掉某种情况,没有编译期检查
return "\"" + value + "\"";
}
// ===== 新写法(sealed + switch,编译期穷举检查)=====
static String serialize(JsonValue value) {
return switch (value) {
case JsonNull n -> "null";
case JsonBool b -> String.valueOf(b.value());
case JsonNumber n -> {
double d = n.value();
// 整数不显示小数点
yield d == Math.floor(d) ? String.valueOf((long) d) : String.valueOf(d);
}
case JsonString s -> "\"" + escapeString(s.value()) + "\"";
case JsonArray arr -> {
var sb = new StringBuilder("[");
var elements = arr.elements();
for (int i = 0; i < elements.size(); i++) {
if (i > 0) sb.append(",");
sb.append(serialize(elements.get(i)));
}
yield sb.append("]").toString();
}
case JsonObject obj -> {
var sb = new StringBuilder("{");
boolean first = true;
for (var entry : obj.fields().entrySet()) {
if (!first) sb.append(",");
sb.append("\"").append(entry.getKey()).append("\":");
sb.append(serialize(entry.getValue()));
first = false;
}
yield sb.append("}").toString();
}
// 不需要default!编译器确保所有情况都被处理
};
}
// Pretty print(缩进格式化)
static String prettyPrint(JsonValue value, int indent) {
String pad = " ".repeat(indent);
return switch (value) {
case JsonNull n -> "null";
case JsonBool b -> String.valueOf(b.value());
case JsonNumber n -> String.valueOf(n.value());
case JsonString s -> "\"" + s.value() + "\"";
case JsonArray arr when arr.elements().isEmpty() -> "[]";
case JsonArray arr -> {
var sb = new StringBuilder("[\n");
var elems = arr.elements();
for (int i = 0; i < elems.size(); i++) {
sb.append(pad).append(" ");
sb.append(prettyPrint(elems.get(i), indent + 1));
if (i < elems.size() - 1) sb.append(",");
sb.append("\n");
}
yield sb.append(pad).append("]").toString();
}
case JsonObject obj when obj.fields().isEmpty() -> "{}";
case JsonObject obj -> {
var sb = new StringBuilder("{\n");
var entries = new ArrayList<>(obj.fields().entrySet());
for (int i = 0; i < entries.size(); i++) {
var entry = entries.get(i);
sb.append(pad).append(" \"").append(entry.getKey()).append("\": ");
sb.append(prettyPrint(entry.getValue(), indent + 1));
if (i < entries.size() - 1) sb.append(",");
sb.append("\n");
}
yield sb.append(pad).append("}").toString();
}
};
}
static String escapeString(String s) {
return s.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\t", "\\t");
}
public static void main(String[] args) {
JsonValue json = new JsonObject(Map.of(
"name", new JsonString("Alice"),
"age", new JsonNumber(30),
"active", JsonBool.TRUE,
"scores", new JsonArray(List.of(
new JsonNumber(95), new JsonNumber(87), new JsonNumber(92)
)),
"address", new JsonObject(Map.of(
"city", new JsonString("Beijing"),
"country", new JsonString("China")
))
));
System.out.println("Compact: " + serialize(json));
System.out.println("\nPretty:");
System.out.println(prettyPrint(json, 0));
}
}四、踩坑实录
坑1:permits必须是直接子类型
// 错误:permits里的类必须直接扩展密封类型
sealed class A permits B, C {}
class B extends A {}
class C extends A {
// C的子类不能出现在A的permits里(除非C也是sealed并有自己的permits)
}
// 正确:多级密封
sealed class A permits B, C {}
final class B extends A {}
sealed class C extends A permits D, E {} // C继续密封
final class D extends C {}
final class E extends C {}坑2:文件约束(同包或嵌套)
// sealed类和其permits中的类必须在同一个编译单元(包)内
// 或者permits的类是密封类的嵌套类
// 错误:permits中引用了不同包的类
// package com.example.a;
// public sealed class MyClass permits com.example.b.SubClass {} // 编译错误
// 正确:同包
// package com.example;
// public sealed class MyClass permits SubClass {}
// public final class SubClass extends MyClass {} // 同包内
// 或者用嵌套类(如前面OrderStatus的例子)
public sealed interface OrderStatus permits OrderStatus.Paid, OrderStatus.Closed {
record Paid(...) implements OrderStatus {}
record Closed(...) implements OrderStatus {}
}坑3:non-sealed破坏穷举性
sealed interface Shape permits Circle, CustomShape {}
final class Circle implements Shape {}
non-sealed class CustomShape implements Shape {} // 开放继承
// 问题:switch穷举性检查不能保证CustomShape的所有子类都被处理
double area(Shape shape) {
return switch (shape) {
case Circle c -> Math.PI * c.radius * c.radius;
// 需要加default,因为CustomShape有未知子类
case CustomShape cs -> cs.calculateArea(); // 或者default处理
};
}
// 教训:谨慎使用non-sealed,它会让穷举性检查失效坑4:反射和序列化的影响
// 反射获取sealed类的permitted子类
Class<?> shapeClass = Shape.class;
Class<?>[] permitted = shapeClass.getPermittedSubclasses();
for (Class<?> c : permitted) {
System.out.println("Permitted: " + c.getName());
}
// 注意:getPermittedSubclasses()返回的是直接子类,不是递归的
// 可以用这个特性在运行时进行穷举性检查或注册处理器
// 序列化:Sealed Classes + Record的组合可以安全序列化
// 但需要确保所有Record都可序列化(实现Serializable或用JSON序列化)坑5:与接口default方法的权衡
// 选择1:在sealed接口里定义default方法(行为在接口里)
sealed interface Shape permits Circle, Rectangle {
double area(); // 抽象方法,每个子类必须实现
default String describe() { // default方法
return "Shape with area: " + area();
}
}
// 选择2:在外部switch里处理(行为在调用者里)
// 两种方案都有合理的用例:
// - 如果所有Shape共享的行为:用接口的抽象方法
// - 如果根据Shape类型做不同处理的访问者逻辑:用switch
// 推荐:核心行为在接口,类型分发逻辑在switch五、总结与延伸
5.1 Sealed Classes的设计原则
适合用Sealed Classes的场景:
✓ 状态机(订单状态、支付状态)
✓ 表达式/AST(编译器、规则引擎)
✓ 结果类型(Success/Failure)
✓ JSON/XML等数据类型建模
✓ 事件类型体系
不适合的场景:
✗ 开放的扩展点(用接口)
✗ 需要第三方继承的基类(用abstract class)
✗ 简单的枚举(状态少且无附加数据,用enum)5.2 与枚举的对比
| 特性 | enum | sealed |
|---|---|---|
| 每种类型携带不同数据 | 不支持 | 支持(配合Record) |
| 穷举性检查 | 支持 | 支持 |
| 继承行为 | 不支持 | 支持(多态方法) |
| 单例保证 | 支持 | 不支持(每次new新实例) |
| 序列化 | 简单 | 需要额外处理 |
5.3 版本兼容建议
- JDK15~16:Preview,不推荐生产使用
- JDK17(LTS):GA,可以使用,但Switch Pattern Matching还是Preview
- JDK21(LTS):Sealed Classes + Switch Pattern Matching + Record Patterns全部GA,是最佳组合
