上周被分到一个工单,要求给订单处理模块增加一种新的支付方式。打开代码一看,满屏的instanceof和层层嵌套的if-else,光搞清楚处理流程就花了我整整一个下午。改完业务之后实在忍不了,花了一天时间用Java 21的模式匹配和Record把整个模块翻了一遍。改完之后代码行数从200多行掉到50行不到,bug还不小心顺带修了两个。趁这次经历还新鲜,把重构的步骤和方法完整记下来。
先看看原来的代码长什么样
这个订单模块的核心逻辑是根据不同的订单类型和支付状态,执行对应的处理流程。简化后的旧代码大概是这样:
public void processOrder(Object order) {
if (order instanceof OnlineOrder) {
OnlineOrder online = (OnlineOrder) order;
if (online.getPaymentStatus() == PaymentStatus.PAID) {
// 已支付在线订单的处理逻辑
fulfillOnlineOrder(online);
} else if (online.getPaymentStatus() == PaymentStatus.PENDING) {
// 待支付在线订单的处理逻辑
remindPayment(online);
} else if (online.getPaymentStatus() == PaymentStatus.CANCELLED) {
cancelOnlineOrder(online);
}
} else if (order instanceof OfflineOrder) {
OfflineOrder offline = (OfflineOrder) order;
if (offline.getPaymentStatus() == PaymentStatus.PAID) {
fulfillOfflineOrder(offline);
} else if (offline.getPaymentStatus() == PaymentStatus.PENDING) {
holdOfflineStock(offline);
}
} else if (order instanceof PreOrder) {
PreOrder pre = (PreOrder) order;
if (pre.getPaymentStatus() == PaymentStatus.PAID) {
scheduleProduction(pre);
} else if (pre.getPaymentStatus() == PaymentStatus.PENDING) {
notifyPreOrderPending(pre);
}
} else {
throw new IllegalArgumentException("未知订单类型");
}
}
这还只是三种订单类型、几种支付状态的组合,实际项目里还有退款订单、换货订单、虚拟商品订单等等,状态也远不止三个。每加一种新类型,这个方法的行数就指数级膨胀。更要命的是,不同的人往里面加逻辑,各自的命名习惯和判断顺序都不一样,时间一长完全变成了一锅粥。
团队里几次提出要重构,但一看到这么多分支就头疼,没人愿意碰。这次趁Java 21已经在生产环境跑稳了,我决定用模式匹配和Record把这块彻底理顺。
第一步:用Record替代普通POJO
原来的订单类都是用@Data注解的普通JavaBean,Getter、Setter、构造器一大堆。这本身不是问题,但订单对象在创建后其实不应该再被修改,用普通Bean没法表达“不可变”这个语义,容易在后续处理中被误改。
我把三个订单类全改成了Record:
public record OnlineOrder(String orderId, PaymentStatus paymentStatus, String paymentMethod) {}
public record OfflineOrder(String orderId, PaymentStatus paymentStatus, String storeCode) {}
public record PreOrder(String orderId, PaymentStatus paymentStatus, LocalDate expectedDate) {}
一行代码搞定,构造器、Getter、equals、hashCode、toString全部自动生成,而且天然不可变。外面的处理逻辑再也不用担心不小心改了订单状态。
第二步:用模式匹配消灭instanceof和强制转型
旧的instanceof后面必须跟一个强制转型,又啰嗦又容易出错。Java 16开始引入的模式匹配可以把这两步合二为一,到了Java 21这个特性已经非常成熟。原来的代码可以改成:
if (order instanceof OnlineOrder online) {
// 直接用online,不需要再转型
if (online.paymentStatus() == PaymentStatus.PAID) {
fulfillOnlineOrder(online);
}
// ...
}
这一步虽然看着只是少了一行转型,但消除的是长期以来Java开发者一直默默承受的小痛点。更重要的是,模式匹配为下一步 switch 的改造铺好了路。
第三步:用switch表达式加模式匹配一次性处理所有分支
Java 21的switch已经完全支持模式匹配,可以直接对对象类型和属性值进行匹配。重构后的核心逻辑变成了这样:
public void processOrder(Object order) {
switch (order) {
case OnlineOrder online && online.paymentStatus() == PaymentStatus.PAID ->
fulfillOnlineOrder(online);
case OnlineOrder online && online.paymentStatus() == PaymentStatus.PENDING ->
remindPayment(online);
case OnlineOrder online && online.paymentStatus() == PaymentStatus.CANCELLED ->
cancelOnlineOrder(online);
case OfflineOrder offline && offline.paymentStatus() == PaymentStatus.PAID ->
fulfillOfflineOrder(offline);
case OfflineOrder offline && offline.paymentStatus() == PaymentStatus.PENDING ->
holdOfflineStock(offline);
case PreOrder pre && pre.paymentStatus() == PaymentStatus.PAID ->
scheduleProduction(pre);
case PreOrder pre && pre.paymentStatus() == PaymentStatus.PENDING ->
notifyPreOrderPending(pre);
default ->
throw new IllegalArgumentException("未处理的订单类型或状态");
}
}
这里用到了Java 21新增的when子句(用&&连接),可以在模式匹配的同时检查属性值。每个分支的意图一眼就能看清,不再需要翻阅嵌套结构。而且因为有default分支兜底,编译器还能检查出没有覆盖的组合,避免漏掉处理逻辑。
第四步:引入密封类,让编译器帮我们检查遗漏
上面的switch虽然清晰,但Object order这个入参类型太宽泛了,谁都能往里面扔一个奇怪的东西。用密封类可以限定订单类型的范围,而且编译器还会强制我们处理所有子类型。
定义一个密封接口:
public sealed interface Order permits OnlineOrder, OfflineOrder, PreOrder {
String orderId();
PaymentStatus paymentStatus();
}
让三个Record实现这个接口:
public record OnlineOrder(String orderId, PaymentStatus paymentStatus, String paymentMethod) implements Order {}
public record OfflineOrder(String orderId, PaymentStatus paymentStatus, String storeCode) implements Order {}
public record PreOrder(String orderId, PaymentStatus paymentStatus, LocalDate expectedDate) implements Order {}
然后把processOrder的入参类型从Object改成Order,switch里的default分支就可以去掉了,因为编译器知道所有可能的类型都已经列出。如果以后有人新增一个RefundOrder类,只要不改permits列表,编译器就会报错,强制提醒修改这里的处理逻辑。
最终版本:清晰得像一份说明书
把以上几步全部做完之后,最终的processOrder方法变成了下面这个样子:
public void processOrder(Order order) {
switch (order) {
case OnlineOrder online when online.paymentStatus() == PaymentStatus.PAID ->
fulfillOnlineOrder(online);
case OnlineOrder online when online.paymentStatus() == PaymentStatus.PENDING ->
remindPayment(online);
case OnlineOrder online when online.paymentStatus() == PaymentStatus.CANCELLED ->
cancelOnlineOrder(online);
case OfflineOrder offline when offline.paymentStatus() == PaymentStatus.PAID ->
fulfillOfflineOrder(offline);
case OfflineOrder offline when offline.paymentStatus() == PaymentStatus.PENDING ->
holdOfflineStock(offline);
case PreOrder pre when pre.paymentStatus() == PaymentStatus.PAID ->
scheduleProduction(pre);
case PreOrder pre when pre.paymentStatus() == PaymentStatus.PENDING ->
notifyPreOrderPending(pre);
}
}
注意这里用了when关键字而不是之前的&&。Java 21最终版把&&改成了when,语义上更明确:这是模式匹配的守卫条件,而不是普通的布尔操作。我最初写的时候还是用&&,编译器报了个预发行API的警告,换成when之后才彻底合规。
整个方法从200多行缩到了不到20行,而且每个业务分支的对应关系一目了然。新加入的同事看了五分钟就明白整个订单处理框架,这在以前根本不敢想。
迁移过程中的几个小插曲
重构过程还算顺利,但也踩了几个坑:
- 同时匹配两个属性值的写法。一开始我尝试写成
case OnlineOrder(PaymentStatus.PAID) online,以为能直接解构Record,结果发现Java 21还不支持Record模式解构,这个功能还在预览。当前只能用when子句检查属性值。不过听说Java 22已经把Record模式转正了,后续可以直接解构。 - 枚举值的导入。switch的分支里如果使用枚举值,必须写全限定名或者提前静态导入,否则编译器会报错。这点一开始让我卡了十分钟,查了一圈才发现是模式匹配对类型推断要求更严格。
- 覆盖率插件的误报。我们用的JaCoCo覆盖率工具对于带
when子句的switch分支识别不太准,会误报某些分支未覆盖。临时加了排除标记,等工具更新之后再去掉。 - 同事的接受度。组里有两位用了五六年Java的同事,第一眼看到
switch搭配箭头语法和when时觉得不像Java。我花了半个小时开了个小型分享会,带着把旧代码和新代码对比跑了一遍,他们当场就接受了,第二天开始在自己的模块里用上了。
少写了代码,多得了什么
代码行数减少只是一方面,更重要的收获在别处:
- 类型安全被真正落实。密封接口保证所有子类型都被处理,不可能再出现“忘了处理某种订单”的bug。
- 不可变性消除了副作用。Record让订单对象在整个处理链路中保持不变,不用再担心某个环节意外篡改了数据。
- 新的业务分支加得快。增加新订单类型只需要新建一个Record实现密封接口,然后在switch里补一条分支,编译器会帮忙定位所有需要修改的地方。
什么时候可以开始用这套写法
模式匹配和Record在Java 21已经是正式特性,不是预览,生产环境可以直接上。密封类也是正式特性。唯一需要注意的是,如果你的项目还在跑Java 17甚至更早的版本,得先安排升级。不过从17升到21的改动不大,Spring Boot 3.2已经全面兼容,整体升级成本可控。
如果你的代码里也有大量instanceof和嵌套if-else在折磨人,建议挑一个相对独立的模块小范围试试。一旦习惯了模式匹配的写法,再看旧代码,会有种想去清理历史遗留的冲动——这种冲动,往往正是重构的最好动力。

