switch 按类型匹配:Java 21 一行干掉 if-else 强转链
系列:Java 后端实战精通营 · 作者:鱼宵
配套课程仓库:https://gitee.com/j67mk2/java21-journey (第 5 课)
先看一段"越长越不敢动"的代码
支付路由,每个后端都写过。微信、支付宝、银行卡……每加一种支付方式,if-else链就加一层;每个分支都是老三样:instanceof 判断 + 手动强转 + 接 else。
等你写到第五种支付方式,这段代码已经长到没人敢重构了——动一行,怕碰坏其它分支。而且类型转错一个,线上直接ClassCastException。
Java 21 的模式匹配 switch就是来终结这种写法的。今天用支付路由这个真实场景,把"老写法 vs 新写法"逐行对照,再讲透when守卫、yield和"不用写 default"背后的穷尽性。
一句话结论:switch 从"比等值"升级成"按类型匹配 + 自动转型"
模式匹配 switch(JEP 441,JDK 21 正式特性):case后面可以直接写类型 + 变量名,匹配上就自动转型并绑定变量——一行顶以前"判断 + 强转 + 绑定"三行。
类比:快递站分拣。老写法是人工分拣:每件包裹拆开看一眼(instanceof)、贴标签(强转)、再送传送带;新写法是分拣机直接看面单品类,传送带自动启动——判断、转型、绑定一步到位。
关键代码:同一笔支付路由,老写法 vs 新写法(完整可运行)
先看"检查依据"——一个 sealed 密封支付家族(上一课学的 sealed 在这里派上大用场):
/** 密封支付家族:permits 名单就三家,这是 switch 能"穷尽"的底气 */publicsealedinterfacePaymentpermitsWeChat,Alipay,BankCard{longcents();// 金额,单位:分}recordWeChat(StringopenId,longcents)implementsPayment{}recordAlipay(StringtradeNo,longcents)implementsPayment{}recordBankCard(StringcardNo,longcents)implementsPayment{}老写法(if-else + instanceof 链):
staticStringrouteOld(Objectpayment){if(payment==null){return"支付方式不能是 null";}elseif(paymentinstanceofWeChat){WeChatw=(WeChat)payment;// 手动强转,转错就是 ClassCastExceptionreturn"微信支付:"+w.openId()+","+w.cents()/100.0+" 元";}elseif(paymentinstanceofBankCard){BankCardb=(BankCard)payment;if(b.cents()>=100000){// 守卫逻辑只能手写 if 嵌进去return"银行卡大额支付:"+b.cardNo()+","+b.cents()/100.0+" 元,需人工审核";}return"银行卡普通支付:"+b.cardNo()+","+b.cents()/100.0+" 元";}else{thrownewIllegalStateException("未知支付方式:"+payment.getClass());}}新写法(模式匹配 switch,同一个需求):
staticStringrouteNew(Paymentpayment){returnswitch(payment){caseWeChatw->"微信支付:"+w.openId()+","+w.cents()/100.0+" 元";caseAlipaya->"支付宝:"+a.tradeNo()+","+a.cents()/100.0+" 元";caseBankCardb when b.cents()>=100000->{// when 守卫子句:先认类型,再按金额细分doubleyuan=b.cents()/100.0;yield"银行卡大额支付:"+b.cardNo()+","+yuan+" 元,需人工审核";}caseBankCardb->"银行卡普通支付:"+b.cardNo()+","+b.cents()/100.0+" 元";casenull->"支付方式不能是 null";};}这段代码讲什么,四句话:
case WeChat w ->:一行同时完成"判断类型 + 自动转型 + 绑定变量",后面直接当WeChat用。老写法的instanceof+ 强转两行,在这里一笔勾销。case BankCard b when b.cents() >= 100000:when 守卫子句——同一个类型,再按值细分分支。1000 元以上的银行卡走大额人工审核,剩下的自然落到下面普通分支。顺序不能反:更具体的(带守卫的)必须放前面。{ ... yield ... }:分支里有多条语句时用花括号包起来,最后yield交出返回值;单表达式分支(case WeChat w -> "...")直接写结果,不用 yield。- 没有
default:为什么敢不写?Payment 是 sealed,三个 permitted 类型全覆盖 +case null接住空值,编译器认定"穷尽了"。
类比 when:银行卡分拣台上的二次扫码。先认出"银行卡"这个大类,再扫一眼金额——超过 1000 的走"大额人工审核"通道,其余走普通通道。
类比 yield:后厨出餐口。这个窗口里炒了好几个步骤(算金额、拼文案),最后一步把成品从出餐口递出来。
再附一个经典等值 switch 的表达式化(输入是 int,不是 sealed 家族,default 还得留着):
returnswitch(day){case1->"周一:写代码";case2->{Stringplan="周二:改 bug";yieldplan+" + 写注释";// 多语句分支必须 yield}case3->"周三:看日志";default->"休息日";// int 编译器不知道全部可能值,default 不能省};箭头语法->天生不贯穿、不用 break——老冒号写法case 1: ... break;漏写 break 的"贯穿"事故,从此绝迹。
实测输出:新写法输出和老写法逐行一致
我本机(JDK 21.0.12.1)的真实输出:
========== 第 5 课:模式匹配 switch ========== --- 1) 老写法:if-else + instanceof 链 --- 微信支付:wx_8801,59.9 元 支付宝:ALI20240001,120.0 元 银行卡普通支付:6222****1234,50.0 元 银行卡大额支付:6222****1234,2000.0 元,需人工审核 支付方式不能是 null --- 2) 新写法:模式匹配 switch(输出应与老写法一一对应)--- 微信支付:wx_8801,59.9 元 支付宝:ALI20240001,120.0 元 银行卡普通支付:6222****1234,50.0 元 银行卡大额支付:6222****1234,2000.0 元,需人工审核 支付方式不能是 null --- 3) 经典等值 switch:箭头语法 + yield --- 周一:写代码 周二:改 bug + 写注释 周三:看日志输出怎么读:
- 老写法、新写法各 5 行输出逐行一致:微信走 WeChat 分支、支付宝走 Alipay、50 元银行卡走普通分支、2000 元银行卡被
when守卫拦进大额分支、null 被case null接住——新写法没有改变业务行为,只是写法升级,这是重构最安全的证据。 2000.0 元那行是{ ... yield ... }多语句分支产的:先算yuan变量,再 yield 拼文案。- 经典等值 switch:
day=1单表达式分支直接出值,day=2多语句走了 yield,全程没有 break。
挑战题(答案都在课程源码里)
- ⭐加支付方式:给 Payment 家族加
record Coupon(String code, long cents)(记得更新 permits),在 routeNew 里加case Coupon c -> ...分支,重新编译跑通。 - ⭐改守卫门槛:把银行卡大额门槛从 1000 元改成 500 元,用 50 元、2000 元两个样例验证分支走向对不对。
- ⭐⭐亲手玩穷尽性:删掉 routeNew 的一个分支,编译,把报错再看一遍——编译器会报 “the switch expression does not cover all possible input values”;然后改回来。
- ⭐⭐老代码吃新语法:把 routeOld 改成
if (payment instanceof WeChat w)模式匹配变量写法——这是 Java 16 的 instanceof 模式匹配,体会"老代码也能先吃一口新语法"。 - ⭐⭐⭐挑战:写一个
feeOf(Payment p)方法(switch 表达式):微信收 0.6% 手续费、支付宝 0.8%、银行卡 1%(大额 2000 元以上按 0.5%),要求用 when 守卫处理银行卡分档,打印四个样例的手续费。
面试回答模板
问:switch 模式匹配解决了什么问题?
- 一句话结论:它把 switch 从"只能比等值"升级成"能按类型匹配并自动转型",消灭了 instanceof + 强转的样板代码。
- 展开 1~2 句原理/例子:case 后直接写类型加变量,同时完成判断和转型;when 守卫子句可以在类型内再按值细分;配合 sealed 家族,分支写全后编译器能做穷尽性检查,漏分支编译就报错,连 default 都不用写。
- 收一句应用场景:我现在写支付路由、消息处理器这种"按类型分派"的代码都用它,路由表一眼看清,比 if-else 链好维护得多。
预判追问:“什么时候不用写 default?”——“输入是 sealed 封闭家族、分支覆盖全部 permitted 类型(并接住 null)时”;“箭头和冒号能混用吗?”——“不能,一个 switch 里只能用一种”;“多语句分支怎么返回值?”——“花括号 + yield”。
总结表:坑 → 原因 → 解法
| 坑 | 原因 | 解法 |
|---|---|---|
| if-else instanceof 链越长越不敢动 | 判断 + 强转 + 业务绞在一起 | 模式匹配 switch,一行完成判断转型绑定 |
ClassCastException | 手动强转类型写错 | case 自动转型,没有强转代码 |
| 箭头和冒号混用编译报错 | 两种语法不兼容 | 一个 switch 只用->(或只用:) |
| 花括号分支忘了 yield | 多语句分支没有交值 | { ... yield 值; } |
| 守卫和兜底顺序写反 | 兜底先接住,守卫永远走不到 | 更具体的(带 when 的)分支放前面 |
| 普通类型省 default 报错 | 穷尽性只对 sealed 家族成立 | int/String 输入保留 default |
| null 输入 NPE | 没显式接 null | case null ->或靠 default |
关于这个系列
本文属于Java 后端实战精通营系列(Java 21 实战 10 课)。完整代码在java21-journey 课程第 5 课(lesson-05/src/main/java/com/java21/lesson05/,含Payment.java密封支付家族 +Main.java老写法/新写法对照):
gitclone https://gitee.com/j67mk2/java21-journey.gitcdjava21-journey/lesson-05 mvn-qcleaninstall-DskipTestsjava-cptarget/classes com.java21.lesson05.Main(仓库地址是小写 j 的 j67mk2,复制别丢字母。跑完会看到老写法、新写法各 5 行输出逐行一致。)
下一篇预告:记录模式——case Order(String id, double amt)这种写法能在匹配的同时把 record 字段"拆"出来,连o.id()都省了。
掏心窝一句:模式匹配 switch 是那种"学了就回不去"的特性。面试官让你写支付路由,你掏出 switch 表达式 + when 守卫,再补一句"配合 sealed 能穷尽、不用 default",这一串下来就是加分项。跑出来的输出和我不一样?评论区一起查。
【发布信息】
- 主标题:switch 按类型匹配:Java 21 一行干掉 if-else 强转链
- 备选标题 2:支付路由 if-else 越写越长?Java 21 模式匹配 switch 让路由表一眼看清
- 备选标题 3:Java 21 模式匹配 switch:when 守卫 + 穷尽性,连 default 都不用写了
- 标签建议:java / Java21 / switch
- 摘要建议:支付路由的 if-else + instanceof 链越写越长、强转还容易翻车。Java 21 模式匹配 switch 让 case 直接按类型匹配并自动转型,when 守卫子句按值细分,配合 sealed 家族实现穷尽性——分支写全连 default 都不用。附支付路由新旧写法对照、实测输出与面试回答模板。