1. 多重选择结构一段代码里的岔路口写 Java 写久了你会发现真正决定代码质量的往往不是那些高深莫测的算法而是最基础的控制流写得够不够干净。多重选择结构就是其中绕不开的一环——程序执行到某个节点时面前摆着好几条路得根据条件选一条走下去。这个场景太常见了订单状态是待支付还是已发货、用户是普通会员还是超级会员、一个分数该落到哪个等级区间全是活生生的多重选择。这篇分享是我把 if-else if-else、switch-case 以及新版 switch 表达式从语法到实操重新梳理了一遍之后整理的笔记思路。适合刚学完 Java 基础语法、准备上手写业务逻辑的初学者也适合写了一段时间但想回头把控制流写得更优雅的开发者。我会把易踩的坑、性能上的差异、以及 Java 12 之后 switch 的现代写法全部揉碎了讲清楚尽量用大白话和实际例子说话不整虚的。很多人觉得多重选择结构不就是几个 if 拼在一起吗有什么好讲的。但真的到了代码评审或者接手老项目的时候你往往会发现同一个逻辑有人写出来层层嵌套一眼到底有人写出来分支套分支半天找不着北。这背后不是语法不会而是对选择结构的理解停留在“能跑就行”的层面。这篇文章想做的就是把这个层级往上拉一拉。2. 为什么要单独把多重选择拎出来讲2.1 一切逻辑分支的底层模型先想一个问题如果没有多重选择结构程序会变成什么样我们确实可以用一堆独立的 if 语句来模拟多重分支代码大概长这样if (status 1) { // 执行 A } if (status 2) { // 执行 B } if (status 3) { // 执行 C }这段代码在语法上完全合法逻辑上也不一定错。但它有个隐蔽的问题如果 status 同时满足多个条件虽然在这个例子里不会但如果条件不是等值判断而是区间判断呢那就会执行多个分支。更麻烦的是每多加一个 if程序都要把所有前面的条件再判断一遍即使已经进入过某个分支也不会停下来。多重选择结构解决的就是这个痛点多个条件互斥时只选其中一条路走条件之间有优先级时按顺序从上往下匹配命中即止。理解这一点你才能真正理解为什么 if-else if 和 switch 要单独学而不是“反正都是 if拼一拼也能用”。它们存在的意义就是让分支意图清晰、执行路径可控、后续可维护。2.2 多重选择的本质是“互斥”和“优先级”设计多重选择逻辑时最关键的两件事是分支之间是否真的互斥如果存在重叠区间谁优先互斥的意思是任意时刻这些条件最多只有一个为 true。最容易犯的错就是把本来互斥的条件写成了多个独立 if结果边界情况同时触发两个分支数据被改了两次bug 查一晚上。优先级的意思是某些情况下多个条件可能都为 true但业务上只能取第一个命中的。经典的例子就是分数等级——80 分既满足“大于 60”也满足“大于 80”的区间条件但我们要的是“满足哪个就先按哪个处理”所以必须从上往下依次判断先命中先执行。这就是试一下和选一下的区别独立 if 是“挨个试一下”多重选择结构是“从第一优先级开始选选中就停”。2.3 用生活场景理解选择结构你下班回家走到小区门口有三个选择超市买菜、直接上楼、去快递站取快递。你的决策逻辑取决于当天情况如果冰箱空了就先去超市如果收到的短信说有快递到柜就先取快递否则直接上楼。这个场景翻译成 Java 就是if (冰箱空了) { // 去超市 } else if (有快递短信) { // 去快递站 } else { // 直接上楼 }这就是多重选择结构最朴素的样子。条件之间有优先级冰箱空了优先于快递互斥性保证了只会走一条路else 兜底了所有意外情况。搞懂这个场景后面所有的语法细节都是在这个模型上加规则。3. if-else if-else 的完整拆解从入门到写明白3.1 基本语法结构if-else if-else 的标准骨架是if (条件1) { // 条件1成立时执行 } else if (条件2) { // 条件1不成立但条件2成立时执行 } else if (条件3) { // 条件1、2都不成立但条件3成立时执行 } else { // 以上条件都不成立时执行 }注意几个关键点每个 else if 前面的条件都隐含着“上面所有条件都不成立”的前提。也就是说你不需要在条件 2 里再写一次“条件1不成立”因为 else 本身已经保证了这一点。else 是可选的。有些场景下没有任何分支命中也是合法状态那就不需要 else。大括号建议永远不要省略。虽然 Java 允许 if 后面不带大括号只跟一行语句但现实是这种写法在后续加代码时极容易引发悬空 else 或逻辑错位代码评审时没人喜欢这种风格。什么时候该用 if-else if-else答案是条件涉及范围判断、复合逻辑与或非、或者分支本身不是简单的等值匹配时。它是最通用的多重选择方案没有之一。3.2 执行顺序与短路原则Java 的多重 if-else if 是从上往下逐条判断的。这个“逐条”意味着两件事第一一旦某个条件命中后面的 else if 和 else 都会被跳过不再执行判断。这个很好理解但很多人写代码时没意识到它对性能的含义如果命中率最高的分支放在最后面那前面所有分支都要白白判断一遍。第二条件本身有短路效果。比如if (list ! null list.size() 0) { // do something }如果 list 是 null那么 后面的 list.size() 根本不会执行直接短路到 false。利用好这一点可以在多重选择里自然规避空指针异常。实操建议把命中概率最高、处理成本最低的判断放在最前面。这个顺序优化看似微小但在高并发接口里一个判断顺序的调整能省下不少无谓计算。3.3 区间判断的边界处理多重 if 最常见的应用场景之一是区间判断比如把分数转等级if (score 90) { grade A; } else if (score 80) { grade B; } else if (score 70) { grade C; } else if (score 60) { grade D; } else { grade F; }这个写法的精髓在于每个 else if 不需要再写上限条件因为前面已经排除了更大的值。第 2 个分支score 80隐含条件是 80 score 90这种“递减式边界”写法简洁且不容易出边界重叠的 bug。反过来如果你在第二个分支里画蛇添足写成else if (score 80 score 90)虽然逻辑没错但等于把前面已经保证的条件重复了一遍纯属浪费阅读成本。边界问题真正的坑在于“等于”的情况。比如上面这个例子如果需求是“90 分及以上为 A89 分还是 B”当前写法没问题但如果某天需求改成“90 分整不算 A”你就得谨慎处理 还是 了。建议把所有边界写清楚最好配合单元测试覆盖等于边界值时的情况别指望靠肉眼检查。3.4 悬空 else 问题悬空 else 是 C 系语言的老问题Java 同样存在。它的本质是else 到底和哪个 if 配对Java 的规则是 else 与最近的未配对的 if 结合。看下面这个经典例子if (a 0) if (b 0) System.out.println(a和b都大于0); else System.out.println(这里表示什么);从缩进看你可能以为 else 属于第一个 if但实际语法规则里它属于第二个 if也就是内层的 if (b 0)。也就是说当 a 0 时这段代码什么都不做而不是打印“这里表示什么”。这就是我说大括号永远不要省略的原因。只要你把内层 if 用大括号包起来再配上一个明确缩进悬空 else 的问题就基本不会发生if (a 0) { if (b 0) { System.out.println(a和b都大于0); } } else { System.out.println(a不大于0); }这是新手最容易踩的坑而且踩了之后往往很难排查——因为语法完全合法只是逻辑和你的预期不一样。4. switch-case 的语法全解与实用模式4.1 从 if 到 switch等值匹配的专门工具如果说 if-else if 是通用的多重选择那 switch 就是专为“单个变量做等值比较”而生的选择结构。它的语法结构switch (expression) { case value1: // 执行逻辑 break; case value2: // 执行逻辑 break; default: // 兜底逻辑 break; }switch 能匹配的类型有限int、char、StringJava 7 开始支持、枚举、以及部分包装类型如 Integer、Character。注意它不能直接用于 long、float、double也不能用于范围比较。比如判断一个分数落在哪个区间switch 就无能为力了。那它存在的意义是什么两个字性能以及可读性。当你的条件是单一变量的等值匹配且有四五个以上分支时switch 的语义更聚焦编译器在某些场景下可以生成跳转表tableswitch而不是逐条比较效率比 if-else if 链高。早期 Java 代码里这种优化在一些解析类工具里被发挥到极致。不过现代 JVM 对 if-else if 也有做优化所以性能差异在业务场景里通常感知不明显真正的价值更多在可读性和意图表达。4.2 穿透fall-through机制是坑也是工具switch 最反直觉的地方就是穿透。如果一个 case 执行完没有 break程序不会停下来而是继续往下执行下一个 case 的代码。看这个例子int day 2; switch (day) { case 1: System.out.println(周一); case 2: System.out.println(周二); case 3: System.out.println(周三); default: System.out.println(其他); }输出结果是周二 周三 其他很多新手第一次看到这个输出都懵了我只想打印“周二”而已怎么后面全跑出来了原因就是没有 break代码一路穿透下去。但穿透不完全是个负面东西它也能被正面利用。比如多个 case 共享同一段逻辑switch (month) { case 1: case 3: case 5: case 7: case 8: case 10: case 12: System.out.println(这个月有31天); break; case 4: case 6: case 9: case 11: System.out.println(这个月有30天); break; case 2: System.out.println(这个月平年28天闰年29天); break; default: System.out.println(无效月份); }这种把多个 case 叠在一起共享逻辑的写法在所有语言里都成立是穿透机制的正向用法。但如果在每个 case 都写业务代码却漏了 break那多半就是 bug 了。经验之谈写完每个 case 的前提是检查 break 是否就位共享逻辑的 case明确写注释说明“这里故意不 break属于共享逻辑”防止后来的人“好心”替你补上 break 反而改坏了功能。4.3 String 和枚举的 switch常用但要注意细节Java 7 之后 switch 支持 String这让代码可读性提升了一大截比如按订单状态字符串来分发逻辑String status PAID; switch (status) { case UNPAID: // 处理未支付 break; case PAID: // 处理已支付 break; case SHIPPED: // 处理已发货 break; default: // 未知状态 }需要注意的一点是switch 对 String 的等值比较是基于equals()方法的所以大小写敏感。如果你的状态值可能在传入时大小写不一致比如 paid 和 PAID 混着来建议先统一格式再 switch。我自己习惯在入口处直接status status.toUpperCase()统一处理杜绝后患。枚举类型的 switch 是另一个高频场景尤其配合状态机模式OrderState state order.getState(); switch (state) { case CREATED: // 刚创建 break; case PAID: // 已支付 break; case COMPLETED: // 已完成 break; }枚举 switch 的好处是编译期就能校验 case 枚举项是否合法写错常量名直接编译报错比字符串拼写错误安全得多。业务状态多的时候我强烈建议用枚举搭配 switch而不是用魔法字符串散落在代码里。4.4 default 分支的最佳实践default 分支在 switch 里对应 if-else 里的 else但使用上有个常见的分歧到底写不写我的观点是如果你的 switch 已经覆盖了所有合法取值尤其是枚举场景default 里建议至少打个日志或者抛个异常因为出现未覆盖的值意味着程序已经处于异常状态沉默处理会把问题藏起来。比如default: // 理论上不可能走到这里 throw new IllegalArgumentException(未知的订单状态: state);这种写法在调试时能快速暴露问题源头。如果是字符串配置类的值default 更要写因为字符串的取值是无限的不可能全部列出 casedefault 就是你的安全网。5. 新版 switch 表达式从语句到表达式的进化5.1 Java 12 引入的箭头语法传统 switch 有多难用写过老 Java 的人都清楚break 写多了心烦穿透机制容易出错而且整个 switch 是“语句”不是“表达式”——它不产出值你只能在外面定义变量然后在里面赋值。但 Java 12 之后switch 有了全新的写法Java 14 正式转正语法变成了箭头风格switch (day) { case 1 - System.out.println(周一); case 2 - System.out.println(周二); case 3 - System.out.println(周三); default - System.out.println(其他); }箭头语法有几个重大改变每个 case 后面的代码执行完自动结束不需要 break。多个常量共用逻辑的写法更简洁case 1, 3, 5 - ...。switch 可以作为一个表达式直接产生值赋值给变量。第三点尤其关键它改变了我们组织多重选择的方式。以前你要在 switch 外面声明变量、在 case 里赋值、还要小心变量被赋了几次现在直接写String result switch (day) { case 1 - 周一; case 2 - 周二; case 3 - 周三; default - 其他; };注意这行末尾有分号因为整个 switch 已经是一个表达式它产出一个 String 值赋给 result。代码量少了一半可读性翻倍。5.2 yield表达式中做更多逻辑箭头语法右边如果只有一行表达式直接用-就够。但有时候你需要在分支里写多行代码最后再产出一个值。这时候就要用到 yield 关键字。先看代码再解释String result switch (day) { case 1 - { // 也许这里要做点额外处理 System.out.println(处理周一逻辑); yield 周一; } case 2 - 周二; default - 未知日期; };yield 的作用是在 switch 表达式的某个分支中产生一个返回值。和 return 不同的是yield 只结束当前分支并返回一个值给 switch 这个表达式本身而不是结束整个方法。这里容易混的一个点是如果用的是传统冒号语法配合 yield要注意作用域和分支覆盖率。但在箭头语法里yield 的使用很自然就是块的返回值。我实际写的时候大都是把复杂分支的逻辑抽成方法然后在箭头右边直接调用方法返回结果这样分支内代码极少yield 用得反而不多。但遇到必须内联的情况比如需要读取分支内局部变量yield 就是唯一解法。5.3 表达式的穷尽性要求switch 表达式和 switch 语句有一个重要区别语句允许没有任何分支匹配不写 default 也没事但表达式必须覆盖所有可能的情况——因为你要给它赋值如果某个值进来没有任何分支命中这个表达式就“没有值”这在语法上是不允许的。所以 Java 对 switch 表达式有一个强制要求穷尽性。具体有两种情况如果 switch 的表达式类型是枚举那么 case 覆盖了所有枚举常量且没有 default是合法的。如果是整数或字符串类型则必须写 default否则编译报错。这个设计我觉得挺合理强制你思考“漏掉的情况怎么处理”而不是让程序在运行时静默地什么都不做。5.4 新版 switch 的适用场景新版 switch 表达式能让代码更紧凑但并不是说所有地方都用它就是最好的。我的使用习惯是纯等值映射、逻辑简单的场景比如状态转文案、类型转描述用 switch 表达式一行搞定赏心悦目。分支内有复杂业务逻辑、每一支都要执行一堆操作不返回值用传统 switch 语句或 if-else if因为这时候表达式返回值反而不必要。分支数量极少两三个且条件简单直接 if-else 更直白没必要上 switch。工具是拿来用的不是拿来炫耀的。选型标准永远是读这段代码的人能否一眼看懂它想干什么。6. 常见问题与排查技巧实录6.1 经典 bug 对照速查表下面整理了几个我在实际开发和 code review 中反复见到的典型问题做成速查表排查时挨个对照现象可能原因解决方案多个分支同时执行用了多个独立 if 而不是 if-else if改成 if-else if 或 switch检查条件设计是否真互斥case 之后代码继续执行到下一分支case 内缺 break检查每个非共享 case 末尾是否都有 break新版语法用箭头自动规避改了输入但 switch 没反应switch 表达式类型不支持该值如 long改用 if-else if或转换类型为支持的类型String 匹配不上但肉眼看着一样忽略了大小写或空格统一使用equals()逻辑入口处做 trim 统一大小写区间条件结果不对条件顺序不对导致高优先级分支被后面截胡理清优先级把最具体的条件放最前面分支里变量重复声明报错传统 switch 的 case 共享同一个作用域给每个 case 加{}大括号创建独立块作用域编译报“不满足穷尽性”switch 表达式缺少 default 或未覆盖全部枚举补上 default 或列全枚举常数6.2 作用域问题的排查传统 switch 有一个很隐蔽的坑所有 case 共享同一个代码块作用域。也就是说你在 case 1 里声明了一个变量case 2 里就不能再声明同名变量否则编译报错switch (x) { case 1: int y 10; // do something break; case 2: int y 20; // 编译报错Duplicate local variable y break; }这个报错在工作区里看起来很奇怪——两个 case 明明不会同时执行为什么变量名不能复用原因就是 Java 语法规定 case 后面不是独立的作用域所有 case 共享 switch 块的作用域。解决方案有两个给每个 case 手动加大括号形成独立块作用域或者用新语法箭头写法箭头语法天然规避这个问题。我建议优先用后者。6.3 字符串切换性能与空指针Java 7 之后虽然支持 String switch但它的实现原理不是直接比较字符串内容而是先计算字符串的哈希码再用哈希码匹配遇到冲突时再调用equals()确认。所以你在 switch 里写case xxx时编译器底层可能已经改造成了哈希 equals 的组合过滤这个过程对开发者透明但能解释为什么字符串 switch 在分支很多时比一串xxx.equals(str)的独立 if 更高效。一个容易被忽略的点是switch 的表达式如果为 null会直接抛出 NullPointerException不管你有没有写case null新版 Java 21 才支持 case null 语法且还在演进中。所以如果你的字符串变量可能是 null在 switch 之前最好先判空if (str null) { // 处理空值 } else { switch (str) { // 正常分支 } }很多线上问题都是传参为 null 时在 switch 处暴雷排查半天最后发现只是漏了判空这个习惯养成不亏。6.4 我的调试经验和建议第一给每个分支入口加日志。不管是 if 还是 switch分支入口处打一条带条件值的日志能让你在日志系统里直接看到每个请求走了哪条路。不然出了线上问题对着代码猜半天到底走了哪个分支效率极低。第二分支逻辑测试要覆盖边界。多重选择结构的 bug 高发区永远在条件边界等于某个临界值、刚好低于临界值、null、空字符串、超大值。写单元测试时这些是必测用例别只测“正常情况”。第三嵌套层级尽量压扁。超过两层以上的嵌套不管是 if 嵌套还是 switch 嵌套阅读成本都剧增。处理办法是提前 return / continue 排除掉非目标分支把代码改写成更扁平的结构。比如if (条件) { if (子条件) { ... } }这种双重嵌套可以改写成两个独立 if 分别 return逻辑梳顺后往往更好懂。第四别过度优化。有不少人喜欢把 if-else if 改成 switch 或者反过来为了所谓的性能差异反复折腾。实际上在业务代码里绝大部分多重选择的性能差异都在毫秒以下真正的瓶颈根本不在这种地方。选择结构的唯一标准是哪个读起来更清楚用哪个。7. 写在最后的一点体会多重选择结构这个东西语法上半天就能学完但真正写好它需要积累。我个人最大的体会是写分支之前先停下来把条件之间的关系理清楚——是互斥还是优先级边界怎么划null 怎么处理——想明白这些再动手写代码基本上就不会有大坑。反过来一上来就噼里啪啦敲代码写完之后再回头检查条件设计很容易陷入改了一处又破坏另一处的泥潭。最后再分享一个小技巧每写完一处多重选择逻辑试着用一句话向旁边的人解释“这段代码在什么情况下会走哪个分支”。如果你自己都说不清楚那说明分支设计还不够干净大概率过几天你自己回来看代码也要想半天。能把条件逻辑用大白话讲清楚的多选择结构才是真正合格的多选择结构。
阅读完成 · 觉得有帮助?