
1. 选择结构代码的岔路口写 Java 写了这么多年我越来越觉得选择结构这事儿特别有意思。你去看任何一个 Java 项目哪怕是微服务架构里跑着的核心交易系统剥到最底层业务逻辑无非就是如果怎么样就做什么否则就做另一件事。这就是选择结构也就是 if、switch、三元表达式这一族语法。很多培训机构和入门教程把选择结构当成三天就能学会的基础语法来讲但真正到了生产环境你会发现同样的一个 if 写出来有人写的代码三个月后自己都看不懂有人写的代码能直接被同事夸清晰得像文档。选择结构解决的核心问题很简单让程序在运行时根据条件做出不同的决策。但它的价值远不止判断这么简单。它是所有复杂业务规则的地基——权限校验要不要放行、优惠券能不能叠加、库存够不够扣减、订单状态能否流转这些全部依赖选择结构来表达。换句话说你写的每一个 if都是在把你的业务规则翻译成机器能执行的语言。这篇文章我把 Java 选择结构从语法细节到工程实践完整梳理一遍。适合刚学完基本语法的初学者也适合那些会用但不深究的开发者。我会把 if-else、switch、三元运算符放在一起对比着讲最后附上我在实际项目中踩过的坑和排错经验希望能省下你自己趟雷的时间。2. if-else 系列最基础也最容易被低估的分支写法2.1 if-else 的完整形态与执行流程先看标准语法。if-else 在 Java 里有三种变体单分支的 if、双分支的 if-else以及多分支的 else-if 链。它们的基本形式长这样// 单分支 if (条件) { // 条件为 true 时执行的代码 } // 双分支 if (条件) { // 条件为 true 时执行 } else { // 条件为 false 时执行 } // 多分支 if (条件1) { // 条件1为 true 时执行 } else if (条件2) { // 条件1为 false 且 条件2为 true 时执行 } else { // 以上条件都不满足时执行 }执行流程其实一句话就能说透从上到下依次判断遇到第一个为 true 的分支就进去执行执行完跳过整个 if-else 结构继续往下走后续的 else-if 即使条件为 true 也不会再执行。这个短路特性是新手最容易忽略的很多 bug 就出在我以为它会把所有匹配的分支都跑一遍。这里有个细节值得强调如果 if 或 else 后面只有一条语句可以省略花括号。比如if (score 60) System.out.println(及格);我不建议你这么写。省略花括号的代码在修改时特别容易埋雷——某天你往 if 分支里加一行日志忘了补花括号逻辑可能就完全变了。我自己的习惯是无论分支里有多少行代码一律加上花括号。这不是洁癖是成本问题。少写两个字符省不了多少时间但一个隐藏 bug 能让你排查半天。2.2 else-if 链多条件判断的直观解法当你有多个互斥条件需要判断时else-if 链是最自然的选择。举个实际例子根据用户积分等级计算折扣率。int points 2450; double discount; if (points 3000) { discount 0.8; } else if (points 2000) { discount 0.85; } else if (points 1000) { discount 0.9; } else { discount 1.0; }这个例子能看出 else-if 几个关键点。第一条件的顺序是敏感的。我把points 3000放在最前面因为它的判断范围最窄应该最先拦截。如果你把宽泛的条件写在前面后面的分支就永远没有机会执行。比如把points 1000放在第一个那 3000 分的用户也会被判定为 0.9 折这就是经典的逻辑覆盖 bug。第二最后一个 else 不是可选的兜底而是覆盖所有未枚举情况的安全网。我强烈建议 else-if 链最后务必接一个 else哪怕只是写日志或者抛异常。因为现实中的输入永远会比你的预设更离谱没有兜底分支时程序会在静默中产生错误结果这比直接报错要可怕得多。第三else-if 链的判断顺序会影响性能。虽然普通业务里这种性能差异可以忽略但如果你的判断体量很大比如几十个分支把命中概率高的条件放前面能减少平均比较次数。这是我在做接口网关时养成的习惯条件路由确实有可感知的延迟差异。2.3 嵌套 if 的代码规范与缩进陷阱嵌套 if 指的是在 if 分支内部再次使用 if 结构用来表达复合条件。比如if (user ! null) { if (user.isVip()) { if (user.getBalance() 100) { // 执行 VIP 且余额充足的操作 } } }三层嵌套就已经让代码的可读性明显下降了五层嵌套基本就是灾难。这种写法的问题在于缩进层级越深阅读者的脑力消耗越大。每多一层嵌套看代码的人就得多记一组条件关系十几个分支叠在一起很容易看串行。我的处理策略有两个方向。第一用逻辑运算符合并条件把多层嵌套拍平成单层。上面的代码可以改写成if (user ! null user.isVip() user.getBalance() 100) { // 执行 VIP 且余额充足的操作 }注意这里 Java 的短路机制从左往右判断一旦user null后面的条件根本不会执行天然避免了空指针。所以先判空再深入操作这个套路用串联既简洁又安全。第二把嵌套逻辑提取成独立方法。当你发现某个 if 里的判断条件超过三个或者嵌套超过两层就应该把条件判断封装成有语义的方法名if (canPlaceOrder(user, order)) { // 下单逻辑 } private boolean canPlaceOrder(User user, Order order) { return user ! null user.isActive() order.getAmount() 0 stockService.hasStock(order.getSkuId()); }这样写最大的好处是主流程里只剩一行 if判断的具体规则被命名了阅读代码的人不需要关心内部逻辑只看方法名就知道这里在做什么。代码的可读性提升了一个档次。3. switch 分支多路判断的正确打开方式3.1 从 JDK 7 到 JDK 14switch 的语法演进很多 Java 老手对 switch 的印象还停留在支持 int、char、枚举小心别漏 break。实际上 switch 这些年变化非常大如果你还在用十年前的老写法真的该更新知识库了。先看传统写法。switch 在 JDK 7 之前只能判断整数和字符类型JDK 7 开始支持 String这一度是很大的进步。传统 switch 长这样String day MONDAY; switch (day) { case MONDAY: System.out.println(周一); break; case FRIDAY: System.out.println(周五); break; default: System.out.println(其他); break; }注意传统 switch 有个贯穿特性如果一个 case 匹配后没有 break程序会继续执行下一个 case 的代码块直到遇到 break 或者整个 switch 结束。这个特性可以用来做多个值映射同一逻辑的处理switch (day) { case MONDAY: case TUESDAY: case WEDNESDAY: case THURSDAY: case FRIDAY: System.out.println(工作日); break; case SATURDAY: case SUNDAY: System.out.println(周末); break; default: break; }但贯穿特性也带来了大量 bug——生产环境里switch 漏了 break 导致逻辑串了的故事可以写一本书。所以从 JDK 14 开始Java 正式引入了增强版 switch彻底改变了这个局面。3.2 表达式式 switch 与箭头语法JDK 12 开始预览、JDK 14 正式发布的增强 switch核心变化是两个箭头语法和switch 表达式。箭头语法用-代替case 值:加 break 的写法每个分支自动结束不再需要 break 关键字String day MONDAY; switch (day) { case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY - System.out.println(工作日); case SATURDAY, SUNDAY - System.out.println(周末); default - System.out.println(非法输入); }多个 case 值可以用逗号合并写在一行可读性比传统写法强太多。箭头右侧既可以是语句块也可以是一个表达式。当 switch 作为表达式使用时它会有返回值String type ADMIN; int permissionLevel switch (type) { case ADMIN - 3; case EDITOR - 2; case VIEWER - 1; default - 0; };这个写法直接解决了根据条件赋值的场景。以前你得先声明变量、写完整 switch、每个 case 里赋值还要记得 break现在一行表达式搞定而且编译器会帮你检查所有可能的情况是否都被覆盖。如果你在 switch 表达式中漏掉了某些可能值且没有 default编译器会直接报错这在传统 switch 里是不可想象的。箭头语法的另一个好处是局部变量作用域更清晰。传统 switch 里你在一个 case 块中声明的变量在另一个 case 块里也看得见因为它们共享同一个作用域经常导致命名冲突。箭头语法每个分支都有自己的独立作用域同名的临时变量在不同分支里可以安全声明。3.3 switch 与 if-else 的选型标准我经常被问什么时候用 switch什么时候用 if-else。我的判断标准很简单当判断条件是基于单个变量的固定值集合时用 switch当判断条件涉及范围、多重条件组合或者复杂表达式时用 if-else。用一张表来对比更直观对比维度if-elseswitch适用场景范围判断、复合条件、任意布尔表达式单变量的固定值匹配可读性分支多时容易臃肿分支多时结构清晰性能逐条比较最坏情况 O(n)编译优化后近似 O(1)类型支持任意类型JDK 7 支持 StringJDK 21 支持模式匹配兜底逻辑else 语句default 分支性能方面传统 switch 在某些 JVM 上会被优化成跳转表tableswitch 或 lookupswitch比逐个 if 比较要快。但说实话在绝大多数业务代码里这个性能差异根本不值得作为选型依据——除非你写的是高频的底层库代码。可读性和可维护性才是第一位的。另外提醒一点JDK 21 开始 switch 还支持模式匹配pattern matching可以直接对对象类型和属性做匹配。这是 Java 语言向现代语言靠拢的重要一步不过目前生产环境中用得还不算多我建议先把基础版本吃透再关注这个进阶特性。4. 三元运算符用表达式替代分支4.1 三元运算符的正确使用方式三元运算符是选择结构的轻量级形态语法是条件 ? 表达式1 : 表达式2。它本质上是一个表达式而不是语句所以它必须产生一个值。这个特性让它特别适合根据条件给变量赋值的场景。int max (a b) ? a : b; String displayName (user.getNickname() ! null) ? user.getNickname() : user.getUsername();写三元运算符时有个原则我特别想强调不要嵌套三元运算符。a ? b : (c ? d : (e ? f : g))这种写法看起来炫技实际上阅读成本极高而且很容易出错误。如果你发现自己写了超过一层的三元嵌套立刻改成 if-else 或者 switch 表达式。三元运算符还有一个容易踩的坑类型不一致导致的隐式转换。比如Object value flag ? 1 : one;这个表达式里 1 是 intone 是 String两者的公共类型是 Object实际上是 Serializable 之类结果没问题。但如果你这么写double result flag ? 1 : 2.5;当 flag 为 true 时1会被自动转换数值提升binary numeric promotion成1.0因为三元运算符要求两个分支的类型经过提升后保持一致。这个机制在大多数情况下是合理的但有些极端场景会带来意外的精度变化。总之两个分支的类型最好写明确避免依赖隐式转换。4.2 三元运算符的使用边界与风格我在代码评审时经常看到有人把三元运算符用在返回语句里这没问题但要控制条件本身的复杂度。比如return (order ! null order.getStatus() OrderStatus.PAID) ? generateTicket(order) : null;这个写法很清爽条件是复合判断也没关系只要它是一眼能看懂的布尔表达式就行。但如果条件很长或者两个分支都是多行逻辑就应该老老实实用 if-else 写清楚。还有一个风格问题是用三元运算符避免空指针还是引入隐患。比如(obj ! null) ? obj.getValue() : null这种写法很常见逻辑上也对。但如果你在方法链上反复用三元判断空值代码会变得极其啰嗦。这种场景我更推荐用 JDK 8 的 Optional 来处理可读性更好。不过这是另一个话题了选择结构的基础还是要先掌握好。5. 实操过程从需求到代码的完整落地5.1 典型场景订单状态流转光讲语法不落地是空谈。我拿一个电商系统里最常见的场景来完整演示订单状态流转。假设订单有以下状态待支付PENDING、已支付PAID、已发货SHIPPED、已完成COMPLETED、已取消CANCELLED。业务规则是待支付订单可以取消已支付订单可以发货也可以申请退款已发货订单可以确认收货已完成和已取消订单不能进行任何操作5.2 代码实现与关键细节先用 if-else 实现一版public boolean canExecute(Order order, OrderAction action) { OrderStatus status order.getStatus(); if (status OrderStatus.PENDING) { return action OrderAction.CANCEL; } else if (status OrderStatus.PAID) { return action OrderAction.SHIP || action OrderAction.REFUND; } else if (status OrderStatus.SHIPPED) { return action OrderAction.CONFIRM_RECEIPT; } else { // COMPLETED 和 CANCELLED 状态 return false; } }这一版功能正确但从工程角度来看有两个问题。第一状态和动作的映射关系散布在多个分支里后续每加一个状态或者一个动作都要改动这段逻辑而且很容易改出遗漏。第二action OrderAction.SHIP || action OrderAction.REFUND这种组合判断在分支多的时候会变得很冗长。用一种更符合工程实践的做法——switch 表达式加枚举组合public boolean canExecute(Order order, OrderAction action) { OrderStatus status order.getStatus(); return switch (status) { case PENDING - action OrderAction.CANCEL; case PAID - action OrderAction.SHIP || action OrderAction.REFUND; case SHIPPED - action OrderAction.CONFIRM_RECEIPT; case COMPLETED, CANCELLED - false; }; }这个写法最大的价值是编译器强制你枚举所有状态如果下次新增一个状态如REFUNDING这里不处理就会编译报错而不是运行时悄悄返回错误结果。这种编译期兜底是增强 switch 表达式最实用的特性我强烈推荐你在生产代码里用起来。当然了如果状态-动作映射关系非常庞大几十种状态几十种动作用 switch 也不合适了那就应该走表驱动设计——用一个二维布尔矩阵或者 Map 来映射状态和动作的合法组合。这里不展开讲只提醒一句当分支数量超过你能一眼看完的极限就该考虑数据驱动的方案了。5.3 重构思路用选择结构写出可演化的代码从上面的代码演化过程你可以看到一个重要的工程思想选择结构不是只能写死在一处它的核心目的是把规则表达清楚。如果你的业务规则经常变化那就应该把规则本身抽象出来。我在实际项目中会选择把某个状态下可以做哪些动作封装成独立的规则枚举public enum OrderStatusRule { PENDING(OrderAction.CANCEL), PAID(OrderAction.SHIP, OrderAction.REFUND), SHIPPED(OrderAction.CONFIRM_RECEIPT), COMPLETED(), CANCELLED(); private final OrderAction[] allowedActions; OrderStatusRule(OrderAction... allowedActions) { this.allowedActions allowedActions; } public boolean canExecute(OrderAction action) { return Arrays.asList(allowedActions).contains(action); } }这样调用方只需要一行代码boolean can OrderStatusRule.of(order.getStatus()).canExecute(action);这是一种典型的用数据换代码的思路把选择结构从密集的 if/switch 变成一张查表。代码量可能没减少多少但每个规则都变成一条清晰的数据记录新增状态、调整权限都只改枚举定义不影响业务调用方。这在状态机、权限系统、流程引擎这些场景下特别实用。6. 常见问题与排查技巧实录6.1 悬空 elseif-else 匹配的经典陷阱悬空 elsedangling else指的是 else 与哪个 if 配对的问题。Java 语法规定else 总是与最近的未配对的 if 匹配而缩进不影响配对。看这段代码if (a 0) if (b 0) System.out.println(a 和 b 都大于 0); else System.out.println(这个 else 到底跟着谁);从缩进看你可能以为 else 跟着第一个 ifa 0但实际上它是跟着第二个 ifb 0。也就是说当a 0为 false 时这个 else 并不会执行。这种 bug 非常隐蔽特别是代码一多、缩进一乱就更容易踩。破解方法就是我前面说的任何 if 都加花括号花括号让配对关系一目了然。6.2 switch 忘记 break 导致贯穿执行传统 switch 漏写 break 是最经典的 Java 坑。你写了一个分支没加 break程序继续往下执行了下一个分支的代码结果变量值被二次修改逻辑错乱。排查方法很简单用-Xlint:fallthrough编译参数编译器会警告所有可能贯穿的 case 块。但更干净的办法还是直接用增强 switch 的箭头语法从语言层面消灭这个问题。6.3 变量作用域与初始化陷阱if 块里声明的变量只在块内有效出了块就访问不到。这个规则衍生出一个很常见的初始化问题如果你想在 if-else 之后使用一个变量必须保证它在所有路径上都被初始化。// 错误示范编译器报错 variable result might not have been initialized int result; if (flag) { result 10; } System.out.println(result); // 编译错误 // 正确做法给 else 也赋值或者声明时就初始化 int result 0; if (flag) { result 10; } System.out.println(result); // OKJava 编译器在这一点上非常严格因为局部变量可能未初始化会导致不可预测的行为。这其实是编译器在帮你堵漏。真正要小心的是另一种情况你用三元表达式赋值时两个分支都没有覆盖到某个值类型编译器会通过类型提升给出一个你可能没预期的结果前面提到过的flag ? 1 : 2.5就是一个例子。6.4 分支预测与性能的真相很多开发者关心 if-else 的性能担心分支多了会影响吞吐。现代 CPU 有分支预测器对于规律性强的判断性能开销极小。但如果你在极端性能敏感的代码里——比如每秒执行百万次的热点路径——写入一段顺序依赖很强的 if 判断分支预测失败branch misprediction的概率会显著升高确实会造成可测量的性能损失。我实际遇到过的一个场景是在高并发网关里用大量 else-if 判断多个开闭规则每个请求都要走一遍。后来我把规则预编译成固定的位掩码表用一次位运算加一次查表替代了十几个分支判断延迟直接降了一个数量级。这个案例说明分支性能问题只有在大规模热路径上才值得专门优化普通业务代码完全不用纠结。先保证代码清晰正确再用性能剖析工具找热点千万别提前优化。6.5 空指针与选择结构的纠缠最后再说一个实战中绕不开的组合在条件里直接调用可能为空的对象方法。if (user.getAddress().getCity().equals(北京)) { // ... }这段代码如果user为 null或者getAddress()返回 null都会抛 NullPointerException。我见过太多线上事故都是这种连点调用导致的。正确的做法是层层判空但层数一多代码就很丑。这个场景我推荐两种处理一是用短路判空if (user ! null user.getAddress() ! null 北京.equals(user.getAddress().getCity())) { // ... }注意我把常量放前面北京.equals(...)这样即使getCity()返回 null也不会抛异常只是得到 false。这个常量在前的习惯帮我避免过无数次空指针。二是用 Optionalif (Optional.ofNullable(user) .map(User::getAddress) .map(Address::getCity) .filter(北京::equals) .isPresent()) { // ... }Optional 链式写法在分支条件长的时候更规整但也不是银弹过度使用 Optional 会让代码也变得难读。我的建议是判空层级超过两层用 Optional两层级以内直接短路。我之前有这样的体会选择结构是整个编程语言里看起来最简单实际坑最多的语法单元。很多程序员觉得 if 写过一万遍不可能出问题但恰恰是这种自信导致了粗心。把每一个分支都当成一次对业务规则的郑重承诺来写多考虑边界、兜底、可读性代码质量自然会上一个台阶。最后分享一个小技巧写完选择结构后花两分钟把所有分支的条件列成一个真值表逐行验证覆盖这个习惯能帮你提前找出九成以上的分支逻辑 bug。