如果你还停留在用 instanceof 加强制转型处理多类型逻辑的阶段,Java 17 到 21 带来的模式匹配会把你的代码洁癖彻底治好。从增强的 instanceof 到功能完备的 switch 模式匹配,再到 record 和密封类,这套组合拳让“数据导向编程”在 Java 里落地了。本文通过一个订单处理系统的重构案例,把这几项特性串起来,让你能直接在项目里用上。
一、告别“instanceof + 强制转型”的样板
在传统 Java 中,判断一个对象的具体类型并提取数据通常要写三行:
if (obj instanceof Order) {
Order order = (Order) obj;
System.out.println(order.getOrderId());
}
Java 16 起,instanceof 支持了模式匹配,可以一行完成:
if (obj instanceof Order order) {
System.out.println(order.getOrderId());
}
变量 order 仅在条件成立时有效,作用域安全。如果条件中有 && 续接,变量在整个条件链中可见:
if (obj instanceof Order order && order.isPaid()) {
// 可以直接使用 order
}
这个简单的改进已经能消除大量冗余代码,但真正的大招在 switch 里。
二、Switch 模式匹配:类型判断与解构一体
Java 21 把模式匹配扩展到 switch。假设我们有多种订单状态类:
public sealed interface OrderStatus permits Pending, Confirmed, Shipped, Cancelled {}
public record Pending(String reason, LocalDateTime since) implements OrderStatus {}
public record Confirmed(String confirmBy, LocalDateTime confirmedAt) implements OrderStatus {}
public record Shipped(String trackingNumber, LocalDateTime shippedAt) implements OrderStatus {}
public record Cancelled(String reason, LocalDateTime cancelledAt) implements OrderStatus {}
使用 switch 模式匹配处理订单状态:
String handleStatus(OrderStatus status) {
return switch (status) {
case Pending(var reason, var since) -> "待处理:" + reason;
case Confirmed(var by, var at) -> "已确认,操作人:" + by;
case Shipped(var tracking, _) -> "已发货,单号:" + tracking;
case Cancelled(var reason, _) -> "已取消,原因:" + reason;
};
}
这里做到了三件事:类型判断、记录组件解构、生成结果,全部内聚在一个表达式里。编译器还会检查分支是否覆盖所有可能的子类型(密封类的功劳),如果漏了某个状态,会直接报编译错误。这在大型项目中避免了漏处理某种状态导致的运行时 bug。
三、Record 类:让数据容器极简化
上面的状态类清一色用了 record。一个 record 自动生成构造器、访问器、equals、hashCode 和 toString,且实例不可变。定义一行就把以前几十行代码搞定:
public record ShippingInfo(String address, String receiver, String phone) {}
在模式匹配中,record 的组件可以直接被解构提取,不需要写 getXxx()。如果你需要自定义逻辑,也可以在 record 体内添加静态方法或实例方法,但组件字段是隐式 final 的,保证了线程安全。
四、密封类:用类型系统约束可扩展性
sealed interface 允许你显式列出所有实现类,这在建模明确的状态机或协议时极其有用。编译器可以利用这一点在 switch 中检查完整性,省掉了永远不访问的 default 分支。
public sealed interface Payment permits CardPayment, WechatPayment, CashOnDelivery {}
public record CardPayment(String cardNum, String bank) implements Payment {}
public record WechatPayment(String openId) implements Payment {}
public record CashOnDelivery() implements Payment {}
当你在 switch 里处理 Payment 时,必须覆盖所有三种支付方式,否则编译不通过。新增一种支付方式时,编译器会引导你补全所有 switch 分支——这就是将运行时错误提前到编译时的最佳实践。
五、实战案例:订单处理系统的重构
考虑一个电商订单处理逻辑:订单有支付状态、配送状态和订单类型三种维度。老式写法会充斥大量 if-else 和 getter 调用,我们现在用新特性让代码回归本质。
5.1 数据模型定义
// 订单支付状态
public sealed interface PaymentStatus permits Unpaid, Paid, Refunded {}
public record Unpaid(LocalDateTime dueDate) implements PaymentStatus {}
public record Paid(String transactionId, LocalDateTime paidAt) implements PaymentStatus {}
public record Refunded(String reason, LocalDateTime refundedAt) implements PaymentStatus {}
// 配送信息
public record Delivery(String address, String contact, String phone) {}
// 订单类型
public sealed interface OrderType permits NormalOrder, PresaleOrder, GroupBuyOrder {}
public record NormalOrder() implements OrderType {}
public record PresaleOrder(LocalDate shipDate) implements OrderType {}
public record GroupBuyOrder(int groupSize, int currentMembers) implements OrderType {}
// 完整的订单记录
public record Order(
String orderId,
OrderType type,
PaymentStatus payment,
Delivery delivery,
List items
) {}
5.2 用嵌套模式匹配处理订单展示
前端需要一个订单详情摘要,要根据不同订单类型和支付状态生成不同的文字描述。过去你会写一堆 if 和 getter,现在可以一步到位:
public String renderOrderSummary(Order order) {
return switch (order) {
case Order(var id, NormalOrder(), Paid(var txId, _), var delivery, _) ->
String.format("订单%s 已支付(流水号:%s),预计送至%s", id, txId, delivery.address());
case Order(var id, PresaleOrder(var shipDate), Unpaid(var due), var delivery, _) ->
String.format("预售订单%s 需在%s前付款,预计%s发货", id, due, shipDate);
case Order(var id, GroupBuyOrder(var size, var cur), Unpaid(_), _, _) ->
String.format("拼团订单%s 还差%d人成团", id, size - cur);
case Order(var id, _, Refunded(var reason, _), _, _) ->
String.format("订单%s 已退款,原因:%s", id, reason);
// 编译器会检查是否覆盖了所有可能组合,如果遗漏则报错
default -> "订单" + order.orderId() + " 状态未知";
};
}
注意这里的 _ 是有名变量的占位符(Java 21 的未命名模式),表示该位置的值不重要,不需要接收。这让代码更简洁,也表明了意图。
5.3 结合 Stream API 与模式匹配
假设需要统计所有需要催促付款的订单(未支付且即将到期),并提取关键信息:
public List getRemindList(List orders) {
return orders.stream()
.filter(order -> order.payment() instanceof Unpaid(LocalDateTime due)
&& due.isBefore(LocalDateTime.now().plusDays(1)))
.map(order -> switch (order) {
case Order(var id, NormalOrder(), _, var delivery, _) ->
id + " - 普通订单 - " + delivery.contact();
case Order(var id, PresaleOrder(var date), _, _, _) ->
id + " - 预售订单,发货日:" + date;
default -> order.orderId();
})
.toList(); // Java 16+ 更简洁
}
instanceof 模式匹配直接用在 filter 里,Unpaid(LocalDateTime due) 同时完成了类型判断和解构,这种写法在过去需要绕一大圈。
六、模式匹配中的陷阱与注意事项
- 完备性检查仅对密封类有效:如果你的接口是开放的(非 sealed),switch 必须带
default分支,否则编译报错。建议在业务模型中使用 sealed 严格控制子类型,既安全又可获得编译器保护。 - 记录解构顺序:解构的顺序与 record 组件声明顺序严格一致,不能跳位也不能用名称匹配。如果需要更灵活的解构,可以结合
var或未命名模式_忽略不需要的组件。 - 模式顺序影响匹配:如果多个模式有重叠关系(如子类型),更具体的模式必须放在前面,否则后面的永远不会匹配。这与 catch 块顺序类似。
- 与旧代码混用:这些特性是 Java 17-21 逐步引入的,如果你的项目还在 11 或 8,可以通过升级 JDK 直接在现有代码中局部采用,渐进式迁移不会破坏原有逻辑。
七、总结
Java 的模式匹配不是语法糖,它改变了我们表达业务逻辑的范式。结合 record 和密封类,代码从一堆指令式的 if-else 转型为基于数据形状的声明式表达。订单处理案例展示了如何在一个真实业务模块中彻底消除强制转型和冗余 getter,让编译器替你检查逻辑覆盖。如果你还在写 if (a instanceof B) { B b = (B) a; },是时候打开 IDE 升级到 21,享受一下干净的代码了。

