资讯动态

Java Switch 表达式:从 JDK 8 保守派到 JDK 21 模式匹配的实战重构

发布时间:2026/9/9 9:32:52 来源:尧图企业网站定制
写了十几年 Java我对 JDK 里新加的语法一向是“能不用就不用”的态度。不是保守而是线上的老代码真的经不起折腾。但有一项特性让我彻底改了这个观念那就是 switch 表达式。从 JDK 14 正式转正到现在我在新代码里已经很少写传统的 switch 语句连很多 if-else 也被我顺手改成了 switch 表达式。这篇是我 JDK 系列的第三篇想完整聊聊为什么我会从一个“保守派”变成 switch 表达式的忠实用户。不管你是刚把 JDK 环境变量配好、还在用 JDK 8 写作业的同学还是已经在面试八股文里被“switch 表达式和 switch 语句有什么区别”问过的在职开发这篇文章应该都能给你一些答案。1. 从一道面试题说起旧版 switch 的最大问题其实是设计1.1 旧 switch 的三大痛点很多 Java 基础面试题里都藏着 switch 的影子“switch 支持 String 吗”“case 里能不能定义变量”“break 忘写会怎样”。这些问题的背后其实是旧版 switch 的结构性缺陷不是靠“细心一点”就能解决的。第一个痛点是 break 穿透。这个从 C 语言时代就传下来的 fall-through 机制坑过无数人。我见过最典型的线上事故就是一个 case 分支少了 break把下一个分支的流程一起执行了。代码长这样int day 3; switch (day) { case 1: System.out.println(周一); case 2: System.out.println(周二); case 3: System.out.println(周三); }day 等于 3 的时候运行结果会打印“周三”但因为没有 break执行完 case 3 会继续往下跑。如果后面还有 case 4、5它们也会被一并执行。这种问题在代码审查里很难一眼看出来因为每个人都知道“应该写 break”但漏写就是漏写了编译器一个错误都不报。第二个痛点是 switch 只能当语句不能当表达式。也就是说你无法直接让 switch 产生一个结果然后赋给变量。现实中大多数 switch 的用途恰恰是“根据条件算出一个值”于是只能先声明一个临时变量然后在各 case 里赋值String result; switch (type) { case A: result alpha; break; case B: result beta; break; default: result unknown; } System.out.println(result);这段代码的问题在于如果某个 case 忘了给 result 赋值变量就是 null编译器不会帮你查。更麻烦的是如果漏了 default输入落在计划外的时候result 可能是前一次遗留的值也可能直接走进不可预期的分支。这个“临时变量 手动赋值”的模式写起来啰嗦还容易留坑。第三个痛点是变量作用域不隔离。在旧版 switch 里多个 case 共享同一个大括号作用域所以在不同 case 里声明同名变量会直接编译失败switch (x) { case 1: int value 1; break; case 2: int value 2; // 编译报错变量 value 已经在作用域中声明 break; }这导致很多人只能给变量起特别绕的名字或者把整个分支逻辑拆到单独方法里。表面看是语法限制其实暴露的是设计问题switch 里的每个分支本来应该是彼此独立的逻辑单元却被强行塞进同一个作用域。1.2 一个从 C 时代就传下来的老包袱要理解为什么这些痛点能存在二十多年得先看看 switch 的历史。Java 语言诞生时很多语法是从 C/C 继承的。C 语言里的 switch 本质就是一张跳转表case 只是跳转标签编译器根本不管你会不会掉到下一个标签里去。这种设计在汇编时代是高效的但对现代开发来说体验很差。问题在于 Java 为了兼容性一直没敢大改 switch 的语义毕竟无数老代码依赖 fall-through 做多条件合并。不过从 Java 8 开始引入 lambda 和 Stream 之后整个社区的编码风格慢慢向“表达式优先”的方向倾斜。大家发现凡是能直接产生值的写法都比“先声明变量再在语句里赋值”要安全、简洁。在这种氛围下Switch 表达式的出现只是时间问题。1.3 switch 表达式的演进时间线Java 保守归保守但真决定改的时候速度也不慢。这里有一条很清晰的演进路径版本JEP内容状态JDK 12JEP 325Switch 表达式首次预览引入箭头语法预览JDK 13JEP 354第二次预览新增 yield 关键字预览JDK 14JEP 361Switch 表达式正式转正正式JDK 17JEP 406模式匹配 for switch 预览支持类型匹配和 null 标签预览JDK 21JEP 441模式匹配 for switch 正式转正正式注意一个容易被忽略的点switch 表达式本身在 JDK 14 就转正了也就是说 JDK 17 这个 LTS 版本里直接就能用不用开任何预览开关。但如果你想用模式匹配比如case String s -这种写法JDK 17 里还得加--enable-preview到 JDK 21 才成为正式功能。这条时间线告诉我们一件事Java 团队很清楚 switch 的顽疾但每一步都走得很谨慎先给社区一年预览期收集反馈再决定是否转正。这种“小步快跑”的策略和很多人印象里 Java 语法“十年不变”的刻板印象完全不同。2. 核心语法拆解switch 表达式到底多了什么2.1 箭头语法告别 break和 fall-through 说再见switch 表达式最直观的变化就是把case 值:换成了case 值 -。String type switch (day) { case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY - 工作日; case SATURDAY, SUNDAY - 周末; default - 未知; };箭头语法的核心语义是每个分支只负责自己的结果不会再掉到下一个分支里去。编译器直接从设计层面消灭了 fall-through你想写“故意穿透”都不行了。这不仅让代码更短还让代码审查可以把注意力从“有没有漏 break”转移到真正的业务逻辑上。另外多标签合并也方便了。以前把多个 case 合并需要这样写case MONDAY: case TUESDAY: case WEDNESDAY: case THURSDAY: case FRIDAY: type 工作日; break;现在可以直接case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -一个逗号分隔的列表就搞定。少写三行也少几个理解负担。我把新旧两种标签语法放在一起对比差别一目了然对比维度旧冒号语法新箭头语法是否需要 break需要漏写导致穿透不需要天然阻止穿透多条件合并连续写多个 case 标签逗号分隔一行搞定分支返回值不能直接返回需要临时变量分支结果就是值变量作用域所有 case 共享每个分支独立可读性细节多、易踩坑更接近业务表达2.2 表达式返回值一次赋值不再漏 default把 switch 从语句变成表达式是这次更新的灵魂所在。所谓表达式就是“会产出值的东西”。以前你写int a 1 2;1 2就是一个表达式它算出 3。现在的 switch 表达式同样可以放进赋值语句里String result switch (status) { case A - alpha; case B - beta; default - unknown; };这个写法的价值在于switch 分支的结果被整体封装在一个表达式里不存在“某个 case 忘记赋值导致变量为 null”的情况。因为每个分支都必须产生一个值如果分支缺少结果编译器直接报错。// 这行代码无法编译 String result switch (status) { case A - alpha; case B - beta; // 没有 default编译器报错 };你可能会问如果确实没有匹配的 case而且我忘了写 default运行时又会发生什么答案是编译器会在 switch 的最后默认插入一个throw new MatchException(...)实际行为是抛出异常从根上杜绝“返回一个未定义值”的尴尬。这种设计比旧版“变量可能为 null”要安全得多。如果你需要直接返回而不是赋值也可以public String getType(String status) { return switch (status) { case A - alpha; case B - beta; default - unknown; }; }switch 表达式不关心你用它来赋值、返回还是作为参数传递它就是一个普通的值。2.3 yield 关键字块状分支怎么返回值箭头语法对单个表达式很方便那如果某个分支需要写多行逻辑再返回结果呢这时候就需要yield。int length switch (name) { case java - 4; case jdk - 3; default - { String trimmed name.trim(); yield trimmed.length(); } };在箭头分支里如果写的是块{ ... }块内必须用yield 值;来产出结果。这个yield相当于“块分支的 return”但它只退出当前 switch 分支把值交给外层表达式。这给了我们很大的自由度简单的分支一行搞定复杂的分支可以写多行最后用 yield 收尾。我记得第一次上手时最不习惯的就是这个关键字老想着直接 return。后来习惯之后发现yield 的语义其实比 return 更精确——它明确告诉你“我要把值吐给 switch 表达式本身”而不是“我要退出整个方法”。另一个细节是箭头分支里如果写多条语句但不产生值那么这里不适合用 switch 表达式。switch 表达式的每个分支必须“有产出”要么是值要么是 throw// 合法某个分支直接抛异常 String result switch (status) { case A - alpha; case B - beta; default - throw new IllegalArgumentException(未知状态: status); };这种写法在参数校验里很常见比在 default 里手动赋一个魔法值要干净得多。2.4 穷尽性检查编译器逼你把分支写全switch 表达式最让我喜欢的一点是它的穷尽性检查。传统 switch 语句里如果你枚举了五种状态但只写了四个 case编译器不会说一句话。等运行时遇到第五种状态代码要么什么都不做要么因为临时变量没赋值而返回 null。这种“漏分支”的 bug 往往要到生产环境才暴露。switch 表达式改变了一切。编译器会检查你的分支是否覆盖了所有可能值如果没有穷尽直接在编译期报错。对枚举类型来说尤其友好enum OrderStatus { CREATED, PAID, SHIPPED, DELIVERED } String label switch (status) { case CREATED - 已创建; case PAID - 已支付; case SHIPPED - 已发货; case DELIVERED - 已送达; // 如果枚举新增一个 CANCELLED这里会编译报错 };这段代码因为覆盖了 OrderStatus 的所有枚举常量可以不用写 default。将来如果有人在 enum 里新增了一个 CANCELLED 状态这个 switch 表达式会立刻编译失败提醒开发者处理新状态。这相当于把一堆隐藏的风险转移到了编译期。对 String 和 int 这类没有穷尽概念的类型default 分支就是必须的否则无法通过编译。这迫使开发者养成“永远考虑未知输入”的习惯从语法层面倒逼代码质量。有同学可能担心既然枚举穷尽后可以省略 default那未来枚举新增值是不是每次都要跑来改 switch我的看法是这恰恰是优点。业务上新增一个状态本来就应该强制你走一遍所有对该状态的判断逻辑编译器帮你找出所有遗漏点。省事的代价可能是线上事故。2.5 模式匹配与 null 处理从 JDK 17 预览到 JDK 21 转正switch 表达式的下一步进化是支持模式匹配。简单说case 后面不仅能写常量还能写“类型模式”结合类型判断来做分支。旧时代我们判断一个对象的类型通常这么写if (obj instanceof String) { String s (String) obj; return s.length(); } else if (obj instanceof Integer) { Integer i (Integer) obj; return i 1; }这段代码有两个烦人的地方一是要做两次类型判断二是强制转换。用模式匹配 switch 可以这么写String desc switch (obj) { case null - null; case String s - 字符串: s; case Integer i - 整数: i; default - 未知类型; };case String s这个写法本身就完成了三件事判断 obj 是不是 String如果是就把 obj 安全转换为 String然后赋值给变量 s。类型判断、强转、变量声明三者合为一体。整个分支从“过程式判断”变成了“模式声明”代码的意图瞬间清晰。模式匹配对 null 的处理也很关键。传统 switch 遇到 null 会直接抛 NPE因为 null 不等于任何 case 值。但在模式匹配的 switch 里case null -成为显式分支可以自己在表达式内部处理空值不需要在 switch 外面单独判空。注意版本差异JDK 17 要开 preview 才能用模式匹配JDK 21 之后才是正式语法。如果你还在 JDK 17 上想用这个特性需要在编译和运行参数里都加--enable-preview。很多团队为了这个特性直接升级到了 JDK 21毕竟预览功能绑在 LTS 版本上老用着也确实不是长久之计。3. 实操案例几个让我彻底“回不去”的重构3.1 案例一订单状态路由用枚举穷尽性换编译期安全我参与过不少订单系统的开发订单状态流转是最典型的 switch 场景。以前写状态处理大概长这样private String handleOrder(Order order) { String desc; switch (order.getStatus()) { case CREATED: desc 新订单等待支付; break; case PAID: desc 已支付等待发货; break; case SHIPPED: desc 已发货运输中; break; case DELIVERED: desc 已送达; break; default: desc 未知状态; } return desc; }这段代码能跑但问题不少第一desc变量在 switch 外面声明和 switch 的逻辑没有强绑定关系第二靠 break 防止穿透少写一个就出事第三default 返回“未知状态”这种魔法值业务上反而可能掩盖真实问题。用 switch 表达式重构之后private String handleOrder(Order order) { return switch (order.getStatus()) { case CREATED - 新订单等待支付; case PAID - 已支付等待发货; case SHIPPED - 已发货运输中; case DELIVERED - 已送达; }; }这里我没有写 default因为 OrderStatus 枚举目前只有这四个值编译器能验证穷尽性。未来如果有人给枚举加了一个 CANCELLED这段代码会立刻编译失败。对我来说这种“编译期报警”比任何测试用例都可靠。如果把 switch 表达式和记录类型record结合写业务 DTO 转换也很顺手。比如把订单实体转换成对外展示对象public OrderVO toVO(Order order) { return new OrderVO( order.getId(), switch (order.getStatus()) { case CREATED - 等待支付; case PAID - 待发货; case SHIPPED - 运输中; case DELIVERED - 已完成; } ); }相比先建一个 VO 对象再一个个 set 字段的写法这种构造器里直接嵌 switch 表达式的方式代码更紧凑而且强制每个字段在构造时就完成赋值不会出现“对象建了一半还缺字段”的中间状态。3.2 案例二类型匹配重构用模式匹配替代 instanceof 加强制转换在项目里处理消息体时我遇到过非常典型的“按类型分发”场景。消息可能是一个字符串、一个数字、一个 JSON 对象也可能是空值。老代码大概是这样public String parseMessage(Object payload) { if (payload null) { return 空消息; } if (payload instanceof String) { String s (String) payload; return 文本消息: s; } else if (payload instanceof Integer) { Integer num (Integer) payload; return 数字消息: num; } else if (payload instanceof List) { List? list (List?) payload; return 列表消息长度: list.size(); } return 未知消息; }这里 if-else 一层层套核心问题在于类型判断和类型转换是分开写的重复代码多而且 if-else 的“顺序”很关键如果把子类型判断放在父类型后面有的分支永远不会执行。用模式匹配 switch 重写后public String parseMessage(Object payload) { return switch (payload) { case null - 空消息; case String s - 文本消息: s; case Integer i - 数字消息: i; case List? list - 列表消息长度: list.size(); default - 未知消息; }; }这段代码的每个分支都自成体系判断、强转、执行全部合并成一条。case List? list甚至可以直接处理泛型通配符不需要再在分支里做二次强转。我特别想强调这个案例的价值模式匹配 switch 能替代的不只是 switch 本身还有大量“先 instanceof 再强转再处理”的 if-else 链条。在很多老项目里这种链条能写上十几层改起来心惊胆战。模式匹配把这种过程式判断变成了声明式分支每个 case 自描述代码读起来就是一份清晰的分发表。3.3 案例三函数式工厂switch 表达式的值也可以是 lambdaswitch 表达式是表达式表达式的值可以是任何类型包括函数式接口。这一点经常被忽略但实际特别好用。举个例子支付场景里不同支付方式对应不同处理逻辑。传统写法会搞一个工厂类在里面用 if-else 或者传统 switch 返回一个处理器对象public PayHandler handler(String payType) { switch (payType) { case WECHAT: return new WechatPayHandler(); case ALIPAY: return new AlipayPayHandler(); default: throw new IllegalArgumentException(不支持的支付方式: payType); } }使用 switch 表达式我们可以直接构造一个“支付类型到函数”的映射FunctionOrder, PayResult handler switch (payType) { case WECHAT - order - wechatService.pay(order); case ALIPAY - order - alipayService.pay(order); case CREDIT_CARD - order - cardService.pay(order); default - throw new IllegalArgumentException(不支持的支付方式: payType); }; PayResult result handler.apply(order);这个方法的核心思路是switch 表达式的每一个分支都是一个 lambda整个 switch 算出一个FunctionOrder, PayResult对象后面再统一 apply。这样一来支付方式的选择逻辑和处理逻辑完全分离以后要加新的支付方式只需要在 switch 里加一行分支不会影响调用方的代码。如果配合枚举写效果更好。毕竟支付方式这种有限集合本来就应该用枚举表达public FunctionOrder, PayResult handler(PayType payType) { return switch (payType) { case WECHAT - order - wechatService.pay(order); case ALIPAY - order - alipayService.pay(order); case CREDIT_CARD - order - cardService.pay(order); }; }因为 payType 是枚举且分支穷尽这里连 default 都不用写。再加上枚举本身不可能出现非法值比 String 分支安全得多。我在这类代码里能找到一种“用语言当 DSL”的感觉分支表一目了然每一条都像一个配置项。4. 常见问题与排查技巧实录4.1 编译报错switch expression does not cover all possible input values这是切换到 switch 表达式后最常见的报错。编译器提示你的 switch 表达式没有覆盖所有可能输入。遇到这个错误先别急着加 default先想想自己用的是不是枚举。如果 switch 参数是枚举最正确的做法是把所有枚举值都写全。这样编译器能验证穷尽性未来枚举新增值也逃不过检查。写 default 反而会让枚举新增值以后“默默通过”失去编译期保护。如果 switch 参数是 String、int 这类没有穷尽概念的类型那就老老实实加上 default。这里的 default 不是可有可无的兜底而是编译器要求你必须考虑“未匹配输入”的处理方式。我通常会在 default 里抛异常而不是返回一个魔法值这样异常输入能尽早暴露。4.2 同一个 switch 里混用冒号和箭头这是新手很容易踩的坑。在一个 switch 里不能同时使用旧冒号语法和新箭头语法。编译器会直接报错。比如这样就是不合法的switch (day) { case MONDAY - System.out.println(周一); case TUESDAY: System.out.println(周二); break; }虽然 Java 语言规范没有禁止“一个 switch 内混用两种 case 标签”的想象空间但目前的实现就是不支持。实际上既然用了箭头语法就没必要再混用。箭头语法本身可以配合块{}执行多条语句只是要记得如果 switch 是表达式每个分支要产出值如果只是语句分支块里正常写代码就行switch (day) { case MONDAY - { System.out.println(周一); log.info(新的一周); } case TUESDAY - System.out.println(周二); default - System.out.println(其他); }注意这里 switch 作为语句使用没有返回值所以每个分支不需要 yield只要执行语句即可。很多人误以为 switch 表达式就必须所有分支都 yield其实 switch 表达式也可以当作语句用只是这样还不如用旧式语句直观。4.3 null 直接判空还是 case nullJDK 21 之前的 switch 表达式传入 null 会抛 NPE。所以如果你还没上 JDK 21面对可能为 null 的入参必须在外层先判空if (str null) { return 空; } return switch (str) { case A - alpha; default - other; };JDK 21 开始case null -可以直接写在模式匹配 switch 里。但请注意这个 null 分支只在模式匹配 switch 中有效。如果 switch 参数还是 String 或 int没有进入模式匹配场景那 null 仍然会 NPE。我见过有人误以为 JDK 21 的 switch 天然支持 null结果线上 NPE其实就是混淆了两种能力的边界。我的实践建议是依赖外部输入时尽量在入口处统一判空只有在内部逻辑里你就是想显式表达“空值也是一种分支”的时候才用case null。4.4 版本与工具链的坑Maven 编译器、IDEA Language Level、preview 开关switch 表达式已经转正很久了但现在很多项目运行在 JDK 8 上代码里自然用不了。就算你本机装的是 JDK 17如果 IDE 的 Language Level 没调对Maven 的编译插件也没指定 source/target编译器一样会按旧语法解析给你报错。在 Maven 项目里pom.xml 至少要配到 JDK 14 以上properties maven.compiler.source17/maven.compiler.source maven.compiler.target17/maven.compiler.target /properties如果你用最新版的 spring-boot-maven-plugin 或者 maven-compiler-plugin建议再显式指定 compiler 的 release 参数plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-compiler-plugin/artifactId configuration release17/release /configuration /plugin如果要用 JDK 17 上的模式匹配 switch 预览功能光改 pom 还不够IDEA 里也要设置 Java Compiler 的 Additional command line parameters 加上--enable-preview运行配置里同样要勾选允许预览。这块配置比较绕我当初也折腾了半天。JDK 21 转正后就没这些事了所以能用 JDK 21 就直接用省心。另外现在的 JDK 下载渠道很多注意认准官方渠道别在杂七杂八的镜像站上下到被篡改的版本。4.5 风格和可读性什么时候不应该用 switch 表达式switch 表达式虽好但不是万能药。我自己的经验是如果分支逻辑主要做副作用——比如打印日志、更新外部对象状态、发送消息——那 switch 语句反而更合适。因为表达式要求每个分支产出值副作用型分支强行 yield 一个 Void 或者布尔值反而让代码变得别扭。// 这种用 switch 语句更自然 switch (event.getType()) { case CLICK: tracker.track(click); break; case VIEW: tracker.track(view); break; default: log.warn(未知事件类型); }如果硬改写成 switch 表达式每个分支都得想办法返回一个值最后还是得调用方忽略这个值纯属画蛇添足。另一个我观察到的问题是有人把某个分支的块写得特别长塞进一大堆业务逻辑再用 yield 把结果吐出来。这种代码可读性不一定好甚至比原来的方法内联还难读。如果某个分支超过十行我建议把分支逻辑抽成独立方法switch 表达式只做路由String result switch (status) { case CREATED - handleCreated(order); case PAID - handlePaid(order); case SHIPPED - handleShipped(order); case DELIVERED - handleDelivered(order); };这样 switch 的分支表本身保持极简复杂逻辑都在各自的 handler 方法里未来的维护者一眼就能看懂整条流程。5. 我个人的使用体会从我自己的实践来看switch 表达式带来的不是说某个功能“终于有了”而是一种思维方式的变化。以前写分支第一反应是“如果这个值等于什么我就去做什么”现在写分支会下意识去思考“这个输入可能有哪些值每个值对应什么结果”。这种从“过程”到“映射”的思维切换写出来的代码天然更容易测试、更容易扩展也更容易保证安全。一个很直观的感受是switch 表达式让 Java 代码和我脑子里的决策表越来越接近。业务上的规则常常就是一张表状态 A 对应动作 X状态 B 对应动作 Y。switch 表达式把这张表直接写进了代码里没有多余的临时变量、没有漏 break 的风险、也不存在“忘了处理某个状态”的隐患。我自从大规模使用之后这类分支代码的线上 bug 数量明显下降大部分问题在编译期就被拦住了。最后分享一个小技巧如果你在重构老代码发现一堆 if-else 都在做“先判断类型再转换再处理”的重复动作别犹豫直接改成 switch 表达式试试。改完之后你会发现代码不是变短了一点而是整个逻辑的层次变得清清楚楚。对我来说这是这几年来 Java 语法演进里少数几个“用了就回不去”的特性我对它的喜欢也是真实写在每一行 commit 里的。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价