
1. 写在前面流程控制编程语言的“交通规则”做了这么多年Java面试过不少人也带过不少新人我发现一个很有意思的现象很多人聊起框架、中间件、分布式头头是道但真让他手写一段包含复杂分支和循环的逻辑时不时就翻车。原因很简单流程控制是Java里最基础的东西基础到很多人觉得“这不就是if和for吗”但越是基础的东西越能看出一个人的功底。我始终认为流程控制就是编程语言的“交通规则”。你写代码本质上是让计算机按照你的意图去执行指令。如果没有流程控制代码就是一条道走到黑从上到下顺序执行完了就结束。但现实世界的业务逻辑哪有这么简单你要根据用户的输入做判断要循环处理一批数据要在特定条件下提前终止某个操作。这些统统离不开流程控制。对于初学者来说流程控制是你敲开Java大门的第一道坎迈过去你就能写出真正“有逻辑”的程序。对于有经验的开发者来说流程控制是代码可读性、健壮性的基石一个简单的if嵌套写不好轻则代码难维护重则埋下线上bug。这篇文章我就结合自己这些年写代码、看代码、改代码的经验把Java流程控制这块彻底聊透。内容不只是讲语法更重要的是讲清楚每个语法背后的设计思路、适用场景、常见坑点以及面试官到底想考你什么。2. 流程控制的整体轮廓三种基本结构在具体讲Java语法之前我觉得有必要先建立一个全局观。早在1966年计算机科学家Böhm和Jacopini就证明了任何复杂的程序逻辑都可以用顺序、选择、循环这三种基本结构来表达。你别看这句话是几十年前的理论到今天依然是铁律。2.1 顺序结构默认的执行方式顺序结构最简单就是代码从上到下逐行执行一行执行完再执行下一行没有跳转没有判断。比如int a 10; int b 20; int sum a b; System.out.println(sum);这段代码没有任何悬念声明变量、赋值、计算、输出一气呵成。平时我们写的大部分“胶水代码”都是顺序结构它是整个程序的骨架。但在真实业务里纯粹的顺序结构非常少因为业务总是充满各种条件判断和分支走向。不过你要明白一点无论程序整体多复杂放在一个方法内部来看每条语句在微观层面依然是按顺序执行的只是流程控制语句改变了“下一条该执行谁”而已。2.2 选择结构让程序学会做决定选择结构就是让程序根据某个条件的真假来决定执行哪一段代码。Java里对应的是if语句和switch语句。这是流程控制里使用频率最高、也最容易写乱的部分。我见过不少新人写选择结构时喜欢把if一层套一层套出五六层深度代码缩进看起来像个金字塔这种代码后来者根本没法维护。选择结构的本质是把“判断题”转换成“路径选择”。你要思考的是什么样的条件该走哪条路条件之间是互斥的还是叠加的有没有边界情况没有覆盖到这些问题想清楚了写出来的代码自然清晰。我在后面会结合例子详细讲。2.3 循环结构让程序学会重复劳动循环结构解决的是“重复做某件事”的问题。Java提供了for、while、do-while三种循环语句适用于不同的场景。循环是程序自动化能力的核心没有循环你要处理100条数据就得写100遍处理逻辑有了循环写一遍就搞定。但是循环也是最容易出bug的地方最常见的两个问题就是“死循环”和“循环边界错误”。死循环就是条件永远为真程序卡死在那里出不来循环边界错误就是多循环了一次或者少循环了一次经典的off-by-one错误。我在面试的时候经常让候选人写一个简单的遍历逻辑目的就是想看看他对循环边界条件的敏感度。这三者组合起来就能表达任何复杂的逻辑。你写任何一个Java程序不管是简单的控制台应用还是庞大的业务系统代码层面都逃不出这三种结构。理解了这一点学流程控制就有了方向感顺序结构不用学天生就会重点攻克的是选择和循环。3. 选择结构实战if与switch的选型和细节选择结构是日常写代码最常用的这里我拆分细讲顺便把面试里高频追问的点也一起说清楚。3.1 if语句的四种形态和使用场景Java的if语句有四种基本形态我整理成一张表方便你对照形态语法适用场景单分支if (条件) { ... }只需要在条件成立时做处理双分支if (条件) { ... } else { ... }条件成立与否分别处理多分支if ... else if ... else ...多个互斥条件从上往下匹配嵌套ifif里再套if多层级条件判断需谨慎使用先说单分支。这个最简单但要注意一个坑即使只有一条语句我也建议你加上花括号。为什么呢因为后续维护时很可能有人觉得“就一条语句不用花括号很清爽”然后往里加了一行代码结果这行代码不在if控制范围内逻辑瞬间就变了。这就是经典的“悬挂else”和“缺少花括号”问题。我自己的习惯是任何if、else、for、while哪怕只有一条语句也一律加花括号。这个习惯救过我很多次。双分支的if-else是最标准的判断结构。需要提醒的是else匹配的是最近的同层级且未配对的if。这个规则在嵌套时会让人犯迷糊所以嵌套时花括号一定不能省。多分支的if-else if我专门说一下。很多人写多分支时直接甩一堆if看着没毛病但性能上和语义上有差别。else if是互斥的只要有一个条件成立后面的条件就不会再判断了而多个独立的if每个条件都会判断一次。如果你的多个条件之间有包含关系顺序就很重要。举个例子判断成绩等级// 正确写法从高到低判断 if (score 90) { System.out.println(优秀); } else if (score 80) { System.out.println(良好); } else if (score 60) { System.out.println(及格); } else { System.out.println(不及格); }这段代码的顺序不能乱。如果先从score 60开始判断那90分的人也会进入“及格”分支因为90也大于等于60。很多人刚学的时候容易犯这个错就是没有想清楚else if的互斥性。3.2 switch语句分支多的场景更优雅switch语句是Java选择结构的另一员大将。在JDK 7之前switch只支持int、char、byte、short以及它们的包装类型和枚举JDK 7开始支持String类型。很多老项目还用着旧写法新项目里我建议能明确类型的情况下多用switch尤其在分支比较多的场景可读性比一串else if好很多。看个例子switch (dayOfWeek) { case 1: System.out.println(周一); break; case 2: System.out.println(周二); break; case 3: System.out.println(周三); break; // ... 其他case default: System.out.println(非法输入); break; }这里有个极其关键的细节break。千万不能漏。如果不写break程序会继续往下执行下一个case这叫“穿透”。有时候我们刻意利用穿透来做一些合并逻辑比如多个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(非法月份); break; }这种写法就是故意不写break让多个case共享同一段逻辑。但很多新手分不清“该穿透”和“不该穿透”于是在不该穿透的地方漏了break导致bug。我的建议是除非你明确要利用穿透否则每个case都写break。这个教训来自一次NPE排查线上的问题是用户传了一个负数switch走到default分支因为漏了break又往下面执行了一段最终把错误数据写进了数据库。从JDK 12开始Java引入了新的switch表达式语法Arrow标签写起来更简洁而且天然不需要breakString result switch (dayOfWeek) { case 1 - 周一; case 2 - 周二; default - 其他; };这个语法在JDK 14以后正式成为标准如果你用的JDK版本比较新推荐优先使用箭头语法代码干净利落也避免了break漏写的风险。3.3 if和switch怎么选关于选型问题我的经验是条件判断比较简单只有两三个分支用if写起来直接明了。分支很多四个以上且判断的是同一个变量的离散值用switch可读性远胜if-else if。判断条件是复杂的逻辑表达式涉及与、或、非组合不能用switch必须用if。判断范围比如区间判断score大于60小于80用ifswitch做不了这种判断。switch本质上是“等值比较”它没法做范围判断。你要是强行把区间拆成一个个case那代码量会爆炸可维护性极差。所以判断区间乖乖用if。4. 循环结构实战三种循环语句的选型与边界循环结构是流程控制的另一个主战场。很多人觉得循环嘛不就是一个for一个while吗真上手写边界条件、遍历方式、性能坑多的是。4.1 for循环定次循环的首选for循环的语法结构是for (初始化; 条件判断; 步进) { ... }。它把循环的四个要素都集中到了一行里结构清晰适合循环次数确定的场景。比如遍历数组int[] arr {1, 2, 3, 4, 5}; for (int i 0; i arr.length; i) { System.out.println(arr[i]); }这里有几个细节值得注意。第一个是循环变量i的作用域它在for循环内有效循环结束就销毁这是推荐写法。如果你把int i 0放在循环外面那i在循环结束后依然存在容易造成变量污染。第二个是i arr.length这个条件我见过很多人喜欢写i arr.length - 1逻辑上没错但不如i arr.length直观。第三个是步进表达式i它是在循环体执行完之后才执行的这个顺序很多人搞错。还有一种更简洁的写法叫增强for循环也就是for-each。它专门用于遍历数组和集合for (int value : arr) { System.out.println(value); }这种写法代码量少也不容易下标越界。但要注意它拿不到下标。如果你在循环里需要用下标做操作增强for就无能为力了。另外用增强for遍历集合时你不能在循环内直接删除元素否则会抛出ConcurrentModificationException。这个异常我在后面问题排查部分还会提。4.2 while与do-while条件驱动的循环while循环适合“不知道循环多少次只知道什么时候停”的场景。比如读取文件直到末尾处理用户输入直到输入某个终止命令while (scanner.hasNext()) { String line scanner.nextLine(); System.out.println(line); }while循环的特点是在进入循环体之前先判断条件如果条件一开始就不成立循环体一次都不会执行。do-while就不同了它是先执行一次循环体再判断条件。也就是说不管条件成不成立do-while至少会执行一次。这个区别在某些场景下很要命。举个我实际遇到的例子做一个用户输入菜单循环int choice; do { System.out.println(请选择功能1. 查询 2. 退出); choice scanner.nextInt(); // 处理逻辑... } while (choice ! 2);这段代码就适合用do-while因为不管用户选什么菜单至少得展示一次。如果你用while就得在循环外再写一遍菜单展示的代码逻辑就重复了。4.3 循环的边界控制与死循环防范循环最怕的就是死循环。死循环不是一定不能用像服务端的监听循环、事件循环本来就是故意写成死循环的。但普通业务代码里出现死循环基本就是bug。最常见的死循环原因是循环变量没有更新或者步进语句因为continue被跳过了。这就引出了跳转语句和循环控制的一个重要搭配问题。我先说一个很典型的边界错误遍历数组时// 错误示范越界访问 for (int i 0; i arr.length; i) { System.out.println(arr[i]); // 当i arr.length时越界 }这里由于写成了数组长度为5但i会走到5arr[5]就必然ArrayIndexOutOfBoundsException。这种错误太常见了基本每个新人都犯过。我的习惯是数组遍历统一用i arr.length集合遍历统一用i list.size()。面试时我偶尔会让对方说说写一个斐波那契数列或者反转字符串本质上都是在考察循环边界控制能力。你能不能在“大脑里”模拟循环的每一轮执行决定了你能不能写出正确的边界条件。所以我建议初学流程控制时老老实实在纸上画出每一轮循环变量变化画几次之后就熟练了。5. 跳转控制break、continue与return的关键差异如果说条件控制决定了“走哪条路”循环控制决定了“做多少次”那跳转控制就决定了“什么时候停下来或者换条路”。5.1 break跳出当前循环break有两个使用场景我分开说。第一个场景是switch分支中用来终止case穿透这个前面已经讲过。第二个场景是循环体中用来提前终止循环。比如在数组中查找某个元素找到了就不用继续遍历了int target 5; int index -1; for (int i 0; i arr.length; i) { if (arr[i] target) { index i; break; // 找到就停不再继续找 } }这种写法的好处是提高效率。如果数组很大目标元素又在很前面用break就能省掉大量无意义的循环迭代。我能看到很多初级代码没有用break明明已经找到目标了还傻乎乎地把数组全部遍历一遍这对于性能敏感的系统是不可接受的。break还有一个容易搞混的点在多层的嵌套循环中break只能跳出最内层的那一层循环。如果你想跳出外层循环Java提供了一个“标签机制”来配合。这个机制用得少但关键时候很有用outerLoop: for (int i 0; i 5; i) { for (int j 0; j 5; j) { if (j 2) { break outerLoop; // 跳出外层循环 } } }这种带标签的break平时写业务代码我基本不推荐因为可读性差。但面试中偶尔会考你了解它的存在即可。5.2 continue跳过本轮剩余的循环体continue的作用是跳过当前这一轮循环中continue之后的代码直接进入下一轮循环。比如打印1到10之间的偶数for (int i 1; i 10; i) { if (i % 2 ! 0) { continue; // 奇数直接跳过 } System.out.println(i); }continue和break的本质区别在于break是整个循环都不要了continue只是这一轮不要了下一轮照常。这个区别你必须刻在脑子里。continue在while循环里有一个容易被坑的点。看这段代码int i 0; while (i 10) { if (i % 2 0) { continue; // 危险i的自增永远不会执行 } System.out.println(i); i; }如果i是偶数continue直接跳到了条件判断i没有自增于是下次循环i还是偶数又continue无限循环程序卡死。这就是我前面说的“步进被continue跳过”导致的死循环。所以在while循环中如果用continue一定要把步进操作放到continue之前或者用for循环替代因为for循环的步进表达式在每一轮结束后都会执行不受continue影响。5.3 return结束整个方法return的作用是终止当前方法的执行并把结果返回给调用方。如果方法返回值是void可以直接return;表示结束方法。在流程控制的语境下return通常和条件判断配合用来做“提前返回”。比如public double divide(int a, int b) { if (b 0) { return 0; // 提前返回避免除零异常 } return (double) a / b; }这种提前返回的写法是一种很好的代码结构。很多新人写代码倾向于在方法里套一层一层if把核心逻辑包在最里面然后各种花括号嵌套。更清晰的写法是先用if把异常情况、非法参数、边界条件全部拦截掉提前return方法体后面只保留主流程逻辑。这就是所谓的“卫语句”Guard Clause。我用这种方式重构过不少老代码嵌套深度大幅下降可读性提升明显。5.4 三道跳转语句的对比总结关键字作用运用场景break跳出当前循环或switch分支找到目标后提前结束循环continue跳过本轮循环剩余部分进入下一轮只处理满足条件的元素return结束当前方法调用返回结果提前终止方法执行卫语句风格这三者的共同点是改变代码的执行顺序这也是为什么它们被归类为流程控制语句。理解了它们的差异你写代码时就能准确选择合适的语句不至于把break和continue混用。6. 常见错误与排查经验这些坑我都踩过写流程控制代码很少有谁能一遍就全对特别是在环境复杂、边界众多的情况下。我把自己带团队时经常看到的问题总结一下也把排查思路分享出来。6.1 死循环排查先看循环条件和步进遇到过死循环第一反应不是急着去看底层的锁或者数据库连接而是先看循环本身。常见的死循环就三个原因条件永远为真、步进语句被跳过比如在while里用continue前移步进、循环变量根本没更新。排查思路很简单在循环体的开头打印循环变量看看它的值到底变不变。如果循环里调用了外部接口还有可能是外部接口阻塞了导致循环没法继续但这已经超出了循环本身的问题。先确认循环变量自身是否正常再往外部排查。6.2 off-by-one错误总是多一次或少一次这种错误特别隐蔽因为代码逻辑看着完全没问题但结果就是不对。最典型的是遍历集合时下意识用了。我建议所有遍历统一用并且对数组长度、集合大小保持高度敏感。另一个容易犯错的地方是分页查询的总页数计算totalPages totalCount % pageSize 0 ? totalCount / pageSize : totalCount / pageSize 1这个表达式里的边界就是典型的off-by-one场景很多人一遍写不对。写的多了你甚至能培养出“边界嗅觉”看到就会本能地停下来多想一秒。这个习惯就是靠踩坑堆出来的。6.3 ConcurrentModificationException遍历时修改集合这个异常几乎是Java集合遍历的“入门劝退题”。当你用增强for遍历一个ArrayList然后在循环体内调用list.remove()删除元素时大概率会触发这个异常。原因是集合的迭代器在创建时会记录一个modCount你删除元素之后modCount变了迭代器下次next()时发现不一致就抛出异常。常规的解决方式有三种使用Iterator并调用iterator.remove()。只收集要删除的元素遍历结束后统一删除。使用Java 8的removeIf方法。这个问题的本质就是你违反了“遍历过程中不要修改结构”的约定。类似的坑还可能在HashMap遍历中出现所以一定要记住这个原则。6.4 switch穿透导致的bug前面已经详细讲过break的问题这里再补一点。有时穿透不是漏写break导致的而是有人故意合并了case但后来者在维护时没有理解这个意图误改了逻辑。这种问题防不胜防最好的办法就是代码评审时重点关注switch块给穿透的部分加上注释明确告诉后来者“这里是有意的”。6.5 浮点数比较不能用在流程控制的判断条件中如果条件涉及浮点数还要特别注意不要用直接比较两个浮点数是否相等。因为浮点数在计算机中是用二进制表示的很多十进制小数无法精确表示比如0.1 0.2 0.3在很多语言里都是falseJava也一样。实际业务中比较浮点数一般用差值是否小于某个极小值来判断。double a 0.1 0.2; double b 0.3; if (Math.abs(a - b) 1e-9) { System.out.println(相等); } else { System.out.println(不相等); }如果你需要高精度的金额计算别用double用BigDecimal并且用它的compareTo方法比较大小而不是equals。这算是我能想到的最值得分享的钱相关经验之一了。7. 从流程控制看代码重构与设计思想流程控制不光是语法它跟代码质量和设计思想紧密相连。我觉得有必要单独写一节说点进阶的思考。7.1 控制流复杂度与可测试性有个很出名的认知指标叫“圈复杂度”它衡量的是代码中独立线性路径的数量。如果一段代码圈复杂度很高说明里头的if、for、switch太多分支和循环交织在一起这样的代码极难测试也极难维护。我见过的线上事故相当一部分都出在这种高复杂度的代码块里。降低圈复杂度最直接的方法就是拆方法。把一个几百行的、嵌套了五六层if的方法拆成多个语义清晰的小方法每个方法只做一件事控制流复杂度自然就降下来了。这也是为什么我一直强调初学者写流程控制时就要有“提取方法”的意识别把所有逻辑堆在main方法里。7.2 卫语句与提前返回前面提过卫语句这里再多说两句。很多老式代码是这样的public void process(Order order) { if (order ! null) { if (order.getStatus() 1) { // 几百行业务逻辑 } else { // 错误提示 } } else { // 参数错误 } }这种写法把主流程和异常处理混在一起阅读时要不断跳转。重构后public void process(Order order) { if (order null) { throw new IllegalArgumentException(订单不能为空); } if (order.getStatus() ! 1) { System.out.println(订单状态不正确); return; } // 这才是主流程 }这个重构思路就是把“不符合条件的”全部拦截在前面方法体中剩下的就是“完全符合条件的”主流程。我自己写代码的习惯是方法前三分之一的代码往往都是卫语句把参数校验、前置条件全部拦完。7.3 Stream与Lambda对流程控制的冲击从Java 8开始StreamLambda很大程度上改变了我们写流程控制的方式。很多以前需要for循环 if判断的代码用Stream可以写得更简洁// 传统写法筛选出年龄大于18的用户名 ListString names new ArrayList(); for (User user : userList) { if (user.getAge() 18) { names.add(user.getName()); } } // Stream写法 ListString names userList.stream() .filter(user - user.getAge() 18) .map(User::getName) .collect(Collectors.toList());很多新人一上来就学Stream反而把基础循环给忽略了。我的看法是两者不是替代关系而是互补关系。简单逻辑用Stream很优雅但一旦涉及复杂的边界控制、循环内依赖多个外部变量、提前终止特定条件下的遍历传统循环依然更合适。而且很多老项目的代码风格还是传统循环为主你能看懂传统循环才能维护老代码也才能更好地理解Stream到底帮你做了什么。8. 实战案例一个流程控制综合应用光讲理论没有用我举一个完整的小案例把选择、循环、跳转、边界控制、卫语句全部串起来。这个案例很典型从控制台读取多个员工的工资统计平均工资、最高工资、最低工资并支持输入-1提前结束。8.1 需求分析需求有三个关键点支持动态输入数量不固定用-1作为结束标志、需要求平均值注意边界条件没有输入任何数据时如何处理、需要记录最高最低值需要初始化策略。这个需求用流程控制来拆解的话用while或do-while循环接收输入因为不知道输入次数只知道遇到-1结束。用if判断当前输入值是否有效以及是否更新最高最低值。用break或循环条件来控制结束。最后顺序结构输出统计结果。8.2 代码实现与解读import java.util.Scanner; public class SalaryStatistic { public static void main(String[] args) { Scanner scanner new Scanner(System.in); int count 0; double sum 0; double max Double.MIN_VALUE; double min Double.MAX_VALUE; boolean validInput false; System.out.println(请输入员工工资输入-1结束); while (true) { double salary scanner.nextDouble(); if (salary -1) { break; } if (salary 0) { System.out.println(工资不能为负数请重新输入); continue; } validInput true; count; sum salary; if (salary max) { max salary; } if (salary min) { min salary; } } if (!validInput) { System.out.println(没有输入有效工资数据无法统计); } else { System.out.println(员工人数 count); System.out.println(平均工资 sum / count); System.out.println(最高工资 max); System.out.println(最低工资 min); } scanner.close(); } }这段代码把本章的很多知识点都用上了。while(true)配合break实现了“死循环 显式跳出”的模式这种写法可以在循环内部多个位置上判断是否退出比在while条件里写复杂逻辑清晰很多。continue用于跳过非法输入直接回到循环开头等待下一次输入。用validInput这个布尔标志来记录是否至少有一次有效输入避免输出“平均工资”时出现除零问题。max初始化为Double.MIN_VALUEmin初始化为Double.MAX_VALUE保证第一次输入就能正确更新极值。这里我再特别讲一下为什么max初始化用Double.MIN_VALUE而不是0因为工资可能全部都是负数如果你的max初始化为0那么无论输入什么负数它都不会大于0最终结果就是错误的。虽然代码里做了负数的拦截但换成其他场景比如统计气温初始值的选择就非常关键。面试里如果你能主动说出这一步选择的理由会很加分。8.3 扩展思考这个案例还可以继续扩展如果要求输入的是字符串怎么办如果要求支持“按名称查询工资”的菜单逻辑怎么办本质上都是流程控制的叠加。你写出一个完整的交互逻辑就会发现流程控制真的是无处不在。掌握好这些基础结构后面学面向对象、学集合框架、学IO都不至于卡壳。9. 面试中流程控制的高频问题与答题技巧结合招聘面试的经验我把流程控制这块常考的问题和答题思路贴出来。这些问题看着简单但想答得让面试官满意需要你展现出深入的理解。9.1 高频问题清单问题考察点答题方向break和continue的区别对跳转语句的理解用例子说明跳出循环 vs 跳过本轮if-else if和switch如何选择选型能力条件类型、分支数量、可读性角度for和while怎么选循环本质的理解定次 vs 条件驱动增强for和传统for的区别遍历机制是否能拿到下标、是否能修改集合do-while和while的区别执行顺序至少执行一次什么情况下会死循环边界意识while里步进被continue跳过等场景写一个字符串反转基础编程能力用for从尾部遍历拼接这些问题不是让你背概念而是让你用自己的话讲清楚。我面试候选人时最反感的是那种照本宣科背定义的人。比如问他break和continue的区别如果他能讲出一个自己踩过的坑说明他是真的理解了。9.2 面试答题技巧回答时先给结论再给例子。比如“break是跳出循环continue是跳过本轮。举个例子我在写查找算法时找到目标用break在过滤数据时不满足条件的用continue。”多提边界情况。面试官问循环你可以主动提off-by-one问题说自己在遍历时习惯用而非这种主动的边界意识很加分。尽量把问题升华一下。比如问if-else和switch你除了回答语法差异还可以提圈复杂度提可维护性提卫语句风格这些都会让面试官觉得你“不只是会用而是有设计意识”。9.3 笔试题中的常见解题思路笔试里经常遇到一类题比如“打印九九乘法表”、“打印菱形图案”、“判断回文数”。这类题的本质都在考察循环嵌套和边界控制。以九九乘法表为例思路是外层循环控制行数1到9内层循环控制列数1到当前行数输出格式用\t对齐。for (int i 1; i 9; i) { for (int j 1; j i; j) { System.out.print(j * i (j * i) \t); } System.out.println(); }这一题能看出你对嵌套循环和边界条件的掌控。内层循环的条件是j i不是j 9这就是关键。笔试时先别急着写代码先在脑子里把i1、i2两轮跑一遍确认边界对了再落笔。10. 最后再说几句经验之谈我带过的每个新人入门阶段几乎都在流程控制上摔过跟头。这不是坏事摔了才知道边界条件有多重要才知道代码执行顺序和人的直觉之间有多大的鸿沟。我想最后再分享几条我在实际工作中始终坚持的习惯。第一永远给流程控制语句加花括号。这个习惯坚持十年可以帮你挡掉无数空指针和逻辑错乱。哪怕只有一个语句也加。第二循环边界统一用“小于”而不是“小于等于”。这能帮你规避掉大部分off-by-one错误。第三能用卫语句提前返回就别把if嵌套得层层叠叠。代码是写给人看的顺便给机器执行让读者读得舒服是职业素养。第四写完流程控制代码一定在脑子里模拟一遍“第一轮”和“最后一轮”的执行过程。这两个边界不出错中间一般不会出错。流程控制这关过了Java的基本功就算真正起步了。后面无论是学数组、集合、字符串处理还是深入面向对象你都会发现所有复杂逻辑最终都是被拆解成一个个选择与循环的组合。把这个地基打牢后面的路就会顺很多。最后再分享一个小技巧调试流程控制代码时别老想着打一堆断点。先看循环变量再判断条件再走一遍逻辑。很多时候问题就出在你自己对执行顺序的认知偏差上。把每一步执行顺序都用日志打印出来你会看得比任何调试工具都清楚。