重构一段 140 行的 if-else 链
1 月初接手支付网关的回调处理模块,打开 NotifyDispatcher 一看,是一个 140 行的 if-else 链。我们新服务用的是 JDK 17(ZGC + G1 都在测),正好可以试试新的语法。
原始代码长这样
public NotifyResult dispatch(NotifyEvent event) {
Object payload = event.getPayload();
if (payload instanceof AlipayNotify) {
AlipayNotify n = (AlipayNotify) payload;
if (n.getTradeStatus().equals("TRADE_SUCCESS")) {
return handleAlipaySuccess(n);
} else if (n.getTradeStatus().equals("TRADE_CLOSED")) {
return handleAlipayClosed(n);
} else {
return NotifyResult.ignored();
}
} else if (payload instanceof WechatNotify) {
WechatNotify n = (WechatNotify) payload;
if (n.getResultCode().equals("SUCCESS") && n.getTotalFee() > 0) {
return handleWechatSuccess(n);
} else {
return NotifyResult.ignored();
}
} else if (payload instanceof UnionpayNotify) {
UnionpayNotify n = (UnionpayNotify) payload;
...
}
return NotifyResult.unsupported();
}
三个毛病:类型转换重复写一遍、每个分支都要重新声明局部变量、分支逻辑和类型判断纠缠在一起。
第一步:instanceof 模式匹配(Java 16 转正)
Java 16 起,instanceof 后面可以直接跟一个绑定变量,类型判断和转型合并:
// 之前
if (obj instanceof String) {
String s = (String) obj;
return s.length();
}
// 之后
if (obj instanceof String s) {
return s.length(); // s 在这里已经完成转型
}
这个变量的作用域是由编译器做流分析得出的,不是简单的"if 块内"。几个容易懵的写法:
if (obj instanceof String s && s.length() > 5) { // OK,&& 短路保证 s 已绑定
...
}
if (obj instanceof String s || s.length() > 5) { // 编译错误,|| 左侧为 false 时 s 未绑定
...
}
if (!(obj instanceof String s)) {
return; // 这里 return 之后
}
System.out.println(s.length()); // OK,编译器知道 s 一定绑定了
第二个例子会报错 variable s might not have been initialized,因为 || 左侧不成立时右侧也会执行,而那时 s 还没绑定。这种"负向判断 + 提前返回"的写法(第三个例子)在实际代码里很常用,编译器能正确推导。
第二步:switch 表达式(Java 14 转正)
支付方式路由这种场景,用 switch 表达式更干净:
// 语句形式,老写法
String channel;
switch (payType) {
case "ALIPAY":
channel = "ch_001";
break; // 忘了 break 就穿透
case "WECHAT":
channel = "ch_002";
break;
default:
throw new IllegalArgumentException("不支持的支付方式: " + payType);
}
// 表达式形式
String channel = switch (payType) {
case "ALIPAY" -> "ch_001";
case "WECHAT" -> "ch_002";
default -> throw new IllegalArgumentException("不支持的支付方式: " + payType);
};
区别不只是少写 break:
- 箭头语法
->不会穿透,不需要break。想穿透必须显式写多个case:case "ALIPAY", "ALIPAY_H5" -> ... - 它是表达式,有返回值,可以直接赋值。
- 穷尽性检查:用作语句时,如果没写
default,编译器不管;用作表达式时,如果没覆盖所有情况且没有default,直接编译错误。
需要多行的分支用 yield 返回值:
int fee = switch (payType) {
case "ALIPAY" -> {
int base = calcBase(amount);
log.info("alipay fee base={}", base);
yield base * 6 / 1000; // yield 是 switch 表达式的返回
}
case "WECHAT" -> amount * 6 / 1000;
default -> 0;
};
yield 是为了和 return 区分开——return 是从方法返回,yield 只是从这个 switch 分支产生值。
第三步:switch 类型模式与守卫(JDK 17 预览)
回到开头那段回调分发。前两步只能解决"类型判断 + 转型"和"值分支",但没法把类型作为 switch 的 case。这个能力在 JDK 17 里是 JEP 406,预览特性,必须加 --enable-preview 才能用:
public NotifyResult dispatch(NotifyEvent event) {
return switch (event.getPayload()) {
case AlipayNotify n when "TRADE_SUCCESS".equals(n.getTradeStatus()) -> handleAlipaySuccess(n);
case AlipayNotify n when "TRADE_CLOSED".equals(n.getTradeStatus()) -> handleAlipayClosed(n);
case WechatNotify n when n.getTotalFee() > 0 -> handleWechatSuccess(n);
case UnionpayNotify n -> handleUnionpay(n);
case null -> NotifyResult.empty();
default -> NotifyResult.ignored();
};
}
三个新东西:
类型模式 case AlipayNotify n
case 标签不再只能是常量,可以是类型 + 绑定变量。运行时按 case 顺序依次做 instanceof 判断,第一个匹配的执行。
守卫 when
类型匹配之后还要再加条件的话,用 when 跟上一个布尔表达式,叫 guard(守卫)。守卫为 false 时继续往下匹配下一个 case,而不是直接走 default。
注意 JDK 17 里守卫关键字是 when,后来的预览版本改成了 &&。这是预览特性的正常演化,别照着新文档往 17 上写。
case null
老 switch 遇到 null 直接抛 NullPointerException。模式匹配 switch 允许显式写 case null,不写的话默认仍然抛 NPE,但会是一个更有信息的 NullPointerException(带 "Cannot invoke ... because the switch selector is null" 之类)。
编译和运行
预览特性要显式打开。Maven 配置:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<release>17</release>
<compilerArgs>--enable-preview</compilerArgs>
</configuration>
</plugin>
运行时也要加:
$ java --enable-preview -jar payment-gateway.jar
WARNING: Using incubator modules: jdk.incubator.concurrent
命令行直接跑单文件的,java --enable-preview --source 17 Test.java 即可。
编译之后会有一个警告:
[WARNING] switch 中的模式匹配是预览功能,可能会在未来版本中删除。
case AlipayNotify n when "TRADE_SUCCESS".equals(n.getTradeStatus()) ->
^
我的建议很明确:预览特性不要上生产。这次重构我在分支里写了完整的实现,但合入主干时用的是 instanceof + switch 表达式的版本——那两个已经是正式特性。类型模式的版本我留在一个 feature 分支里,等它转正(JDK 21)再切过去。
改完之后的数据
NotifyDispatcher.java
改前:142 行,8 个分支,圈复杂度 24
改后:67 行,8 个分支,圈复杂度 11
SonarQube 上的 Cognitive Complexity 从 24 降到 9。可读性提升主要来自两点:少写了 8 次强制转型,以及每个分支的目标方法一眼可见。
写在后面
现在回头看,《switch 模式匹配的演进与实战》本身不算多难,难的是线上真出问题那十分钟里的判断。经验都是这么来的。