Java流程控制详解:分支、循环与跳转的高频考点与避坑指南
兄弟们今天聊点Java里最“无聊”但又最“致命”的东西——流程控制语句。说它无聊是因为但凡学过编程的人谁不知道if、else、for、while啊说它致命是因为我面试过太多人笔试环节手写个二分查找或者冒泡排序居然能在循环边界和条件判断上栽跟头。不夸张地讲流程控制就是Java代码的“骨架”变量和数据类型是“血肉”你骨架搭得歪七扭八后面学什么面向对象、集合框架、并发编程全都是空中楼阁。这一篇Day03我不打算像教科书那样给你罗列语法规则。咱们直接换个视角从“实际写代码会遇到什么坑”和“面试官到底想考你什么”这两个角度把分支、循环、跳转这些老生常谈的东西重新捋一遍。适合那种刚看完Hello World、正准备往深处走一步的初学者也适合那些学了几个月Java、但写出来的代码总感觉“差点意思”的同学。看完这篇你至少能明白为什么别人写循环那么优雅你写循环却总是在死循环边缘疯狂试探。1. 流程控制到底在控制什么其实说穿了计算机执行程序就是一条道走到黑如果没有流程控制那代码就跟流水账一样从上往下念念完就结束。这种“顺序结构”处理简单任务还行但凡遇到“如果余额不足就提示充值否则就扣费”这种场景你就需要让程序“动脑子”选择走哪条路。流程控制语句就是给程序装上“分岔路口”和“回环跑道”。分支语句控制程序往哪个方向走循环语句控制程序重复做某件事多少遍跳转语句则负责在关键时刻强行“改道”或者“刹停”。这三者组合起来你才能描述出复杂的业务逻辑。1.1 顺序结构被忽略的默认值很多人学流程控制一上来就盯着if和for看反而忘了最基本的顺序结构。其实顺序结构才是程序的主干道所有的分支和循环都是在这条主干道上“开岔路”或者“绕圈圈”。int a 10; int b 20; int sum a b; System.out.println(结果 sum);这段代码没啥好解释的从上到下依次执行。但我想提醒你的是如果你在某一行代码里改变了变量的值后面所有用到这个变量的地方结果都会跟着变。很多莫名其妙的bug根源就在于你忽略了语句执行的先后顺序在某个分支里改了不该改的变量导致后面的逻辑全乱套了。1.2 为什么流程控制是“分水岭”我见过不少初学者变量和数据类型学得挺溜一到了流程控制就开始懵。原因很简单前面学的是“死”的东西定义变量、赋个值、做个运算都是有固定答案的。但流程控制是“活”的它要求你像计算机一样去模拟执行脑子里得有一张“程序运行轨迹图”。这个阶段真正考验的不是记忆力而是逻辑推导能力。你能不能看着一段带嵌套循环的代码准确说出它打印出来的每一个数字你能不能设计出一个循环恰好遍历完数组里所有的元素不多不少这才是流程控制的核心。这关过了后面学方法、学递归、学各种算法都是一马平川这关过不了后面写啥都磕磕绊绊。2. 分支结构深度拆解if-else和switch的恩怨情仇分支结构是流程控制的第一个大板块核心就俩if-else和switch。很多人觉得这有啥好讲的不就是个条件判断吗但恰恰是这么简单的东西里面藏着的坑比你想的多得多。2.1 if-else的三种形态与“悬空else”陷阱if语句有单分支、双分支、多分支三种形态。单分支就是if后面只有一条路满足了就执行不满足就跳过双分支就是经典的if-else二选一多分支就是if-else if-else一路判断下去。// 单分支 if (score 60) { System.out.println(及格了恭喜); } // 双分支 if (score 60) { System.out.println(及格了恭喜); } else { System.out.println(不及格继续努力); } // 多分支 if (score 90) { System.out.println(优秀); } else if (score 80) { System.out.println(良好); } else if (score 60) { System.out.println(及格); } else { System.out.println(不及格); }这里有个经典得不能再经典的坑叫“悬空else”。你们看下面这段代码猜猜输出啥int x 5; int y 10; if (x 0) if (y 20) System.out.println(A); else System.out.println(B);很多新手以为else是跟第一个if配对的所以x大于0但y不大于20应该输出B。但实际上Java语法规定else总是与最近的if配对所以这个else是跟if (y 20)配对的。x确实大于0但y不大于20所以走else分支输出B。这个例子还算温和因为结果碰巧一样。如果我把条件换一下比如x不大于0那这个else的执行结果就会彻底出乎你的意料。注意写代码一定要加花括号不要偷懒省略{}。这不仅仅是为了可读性更是为了规避这种“悬空else”的逻辑歧义。我自己写代码哪怕if后面只有一条语句也照样加花括号这就是个习惯问题。另外还有个比较隐蔽的问题条件判断的顺序。看上面多分支的例子如果把score 90和score 80的顺序调换一下你会发现成绩永远都不可能输出“优秀”。因为只要score大于等于80就直接进入“良好”分支了。所以写多分支的时候条件范围一定要从大到小或者从小到大按照严格的顺序来否则就会出逻辑bug。2.2 switch的演进从经典写法到箭头语法如果说if-else是万金油那switch就是精准打击的利器。经典写法大家应该都见过switch (day) { case 1: System.out.println(星期一); break; case 2: System.out.println(星期二); break; default: System.out.println(未知); break; }这个写法最大的坑就是忘记写break。一旦漏了break就会发生“穿透”现象从匹配的case开始一直往下执行直到遇到break或者switch结束。有时候你故意利用穿透来合并逻辑比如case 1和case 2共享一段代码但这属于奇技淫巧新手不建议模仿。Java 14之后switch迎来了大改版引入了箭头语法不用写break也不会穿透了switch (day) { case 1 - System.out.println(星期一); case 2 - System.out.println(星期二); default - System.out.println(未知); }而且switch还能作为表达式直接赋值String type switch (day) { case 1, 2, 3, 4, 5 - 工作日; case 6, 7 - 休息日; default - 非法输入; };看到没用逗号把多个case合并在一起直接返回一个值赋给变量。这种写法简洁清晰新项目里我强烈推荐使用新语法。2.3 分支结构的代码风格建议写分支结构个人有几点建议都是现实开发中会遇到的。第一能早点return就早点return。那种层层嵌套的if-else缩进越来越深读起来跟套娃一样痛苦。建议用卫语句先把异常情况快速返回主干逻辑写在后面。// 不好的写法 public void process(Order order) { if (order ! null) { if (order.getStatus() 1) { if (order.getAmount() 0) { // 业务逻辑... } } } } // 好的写法卫语句 public void process(Order order) { if (order null) { return; } if (order.getStatus() ! 1) { return; } if (order.getAmount() 0) { return; } // 业务逻辑... }第二种写法逻辑上完全等价但阅读体验不知道好了多少倍。你不需要一层一层地剥开外层if才能看到里面真正的业务逻辑。第二分支里的代码块别写得太长。一个if分支里面动辄几十行代码这代码基本就废了。这时候你应该把分支里的逻辑提取成单独的方法。这不只是风格问题更是可维护性问题。3. 循环结构for、while、do-while的取舍之道循环是流程控制的另一个大板块。Java里主要有三种循环for循环、while循环、do-while循环。很多人学的时候觉得选择很多真到写的时候又不知道该用哪个。3.1 for循环的执行顺序拆解for循环的标准结构是for (初始化; 条件判断; 步进)。很多人只知道照葫芦画瓢却不清楚里面每一步的执行时机。我给你们拆一下for (int i 0; i 10; i) { System.out.println(i); }执行顺序是这样的先执行int i 0然后判断i 10成立则进入循环体执行System.out.println(i)然后执行i再次判断i 10……直到判断不成立为止。所以要记住初始化只执行一次条件判断每次都执行步进在每次循环体执行完之后执行。很多新手会在for循环里写出死循环就是因为对执行顺序的理解有偏差。比如for (int i 0; i 10; ) { System.out.println(i); // 忘记写 i }这个循环里i永远不会增加条件永远成立妥妥的死循环。还有更隐蔽的比如在循环体内部修改了循环变量的值导致循环次数完全超出预期。这类问题排查起来特别费劲。3.2 while和do-while的本质区别while循环和do-while循环长相很相似但有一个根本区别while是先判断后执行do-while是先执行后判断。这意味着do-while循环体至少会被执行一次而while循环体可能一次都不执行。// while先判断可能一次都不执行 int i 10; while (i 10) { System.out.println(这行不会被打印); } // do-while先执行一次再判断 int j 10; do { System.out.println(这行会被打印一次); } while (j 10);实际开发中do-while用的场景相对较少但有个典型场景是需要先读取用户输入再根据输入决定是否继续读取。这时候天然就是先做一次再判断do-while就很合适。Scanner scanner new Scanner(System.in); int input; do { System.out.println(请输入一个正数输入负数退出); input scanner.nextInt(); // 处理输入... } while (input 0);至于怎么选循环我的经验是知道循环次数用for不知道循环次数用while。比如遍历数组、遍历列表次数是明确的就用for。读取文件直到文件末尾次数不固定就用while。3.3 循环嵌套与时空复杂度直觉循环嵌套是很多初学者的噩梦特别是双重循环甚至三重循环。其实嵌套循环不难理解关键是要把握住“外层循环走一次内层循环走一遍”这个规律。for (int i 1; i 3; i) { for (int j 1; j 3; j) { System.out.println(i i , j j); } }这段代码会输出9行因为外层循环3次每次内层循环3次3×39。这就是乘法原理。但嵌套循环带来一个更重要的问题时间复杂度。一个双重循环如果内外层都循环n次那总操作次数就是n²。当n是100的时候那就是10000次操作当n是10000的时候那就是1亿次操作。这在真实项目中就会产生性能问题。我给你举个实际例子冒泡排序。这玩意儿在热搜词里出现了确实也是面试高频手写题。冒泡排序就是用双层循环实现的public static void bubbleSort(int[] arr) { int n arr.length; for (int i 0; i n - 1; i) { for (int j 0; j n - 1 - i; j) { if (arr[j] arr[j 1]) { int temp arr[j]; arr[j] arr[j 1]; arr[j 1] temp; } } } }这里内层循环的j n - 1 - i就是优化过的写法。外层每走一遍就会把当前最大的元素“冒泡”到最后面所以内层循环就没必要再去比较已经排好序的那些元素了。这就是性能优化的直觉。第一次跑外层循环内层比较n-1次第二次跑内层比较n-2次最后加起来是(n-1)(n-2)...1 n(n-1)/2时间复杂度还是O(n²)。所以冒泡排序适合教学但实际项目里几乎不用就是因为太慢了。4. 跳转控制break、continue、return的七十二变有了分支和循环程序已经能处理很复杂的逻辑了。但有时候我们需要在循环中间提前结束或者跳过某一次循环。这就需要跳转控制语句。4.1 break和标签跳转break的作用是跳出当前循环。如果在嵌套循环中break只能跳出最内层循环。但有时候我们想从内层循环直接跳出所有循环怎么办Java提供了一个蛮古老的机制标签跳转。outer: for (int i 0; i 5; i) { for (int j 0; j 5; j) { if (j 2) { break outer; } System.out.println(i i , j j); } }这里outer:是个标签break outer就是跳出标签指定的那个外层循环。这个写法在面试中属于加分项知道的人不多。但实际开发中标签跳转用得极少因为这种写法破坏了代码的结构化可读性变差了。如果真遇到需要跳出多层循环的场景更推荐的做法是抽取一个方法用return直接返回。4.2 continue的过滤妙用continue的作用是跳过本次循环剩余的代码直接进入下一次循环。这玩意儿在做数据过滤的时候特别好用。// 打印1~10之间所有的偶数 for (int i 1; i 10; i) { if (i % 2 ! 0) { continue; } System.out.println(i); }这里遇到奇数就continue跳过不执行后面的打印语句。当然这个例子也可以不用continue写成if (i % 2 0) { System.out.println(i); }。但有些场景下continue能大幅减少嵌套层级。比如你要遍历一个列表对符合条件的元素做处理for (Product product : productList) { if (product.getStock() 0) { continue; } if (product.getStatus() ! 1) { continue; } // 对有效商品进行处理... }用continue把不合条件的跳过后面代码就不用缩进套来套去了清爽得很。4.3 return在流程控制中的角色return的作用更直接结束当前方法返回一个值或者不返回值。在循环里用return相当于“一票否决”——只要执行到return整个方法直接结束后面的循环和代码统统不再执行。public static boolean contains(int[] arr, int target) { for (int num : arr) { if (num target) { return true; } } return false; }这个方法用来判断数组里是否包含目标值。一旦找到了直接return true根本不需要遍历完整个数组。这种“提前返回”的思路在算法题里非常常见能省下不少没必要的计算。5. 面试与实战流程控制的高频考点和易错点说了这么多语法细节咱们来点实战向的。这一节我结合面试经验和实际开发踩过的坑整理一下流程控制里最常见的考点和最容易出问题的地方。5.1 循环里的“差一错误”“差一错误”是所有写循环的人都会遇到的老朋友。比如遍历数组数组长度是10下标从0开始合法下标是0到9。你写循环的时候是i 10还是i 10写错了就是数组越界异常。int[] arr {1, 2, 3, 4, 5}; // 正确写法 for (int i 0; i arr.length; i) { System.out.println(arr[i]); } // 错误写法i arr.length 会越界 for (int i 0; i arr.length; i) { System.out.println(arr[i]); // 最后一次访问 arr[5]数组越界 }这个错误报出来的异常是ArrayIndexOutOfBoundsException也就是热搜词里那个“java中数组越界异常”。遇到这个异常第一反应就应该是检查循环的边界条件。这也是面试官特别喜欢让候选人手写循环遍历的初衷——看看你对边界条件敏不敏感。我自己的习惯是凡是循环里要用到数组或者集合的下标一律用i arr.length而不是i arr.length。写完之后心里默数一下边界情况i等于0的时候访问第一个元素i等于length-1的时候访问最后一个元素不多不少刚刚好。5.2 经典面试手写题冒泡排序和它的循环控制细节冒泡排序刚才展示过了这里再补充几个容易出错的地方。第一外层循环为什么是i n - 1而不是i n因为n个元素最多只需要n-1轮冒泡就能排好。如果n5那第4轮结束就已经有序了没必要再跑第5轮。第二内层循环为什么是j n - 1 - i而不是j n - 1因为每一轮冒泡都会把当前最大值放到最后这些已经排好的元素不需要再参与比较。这个- i是优化不写也不会错但写了就是更好的代码。第三交换两个元素的小操作。很多人会用异或运算的花活arr[j] arr[j] ^ arr[j 1]; arr[j 1] arr[j] ^ arr[j 1]; arr[j] arr[j] ^ arr[j 1];不建议这么干这写法看起来很炫但可读性太差而且如果两个数相同会有问题异或结果是0把数据弄丢。老老实实定义个临时变量交换永远不会出错。5.3 增强for循环和普通for循环的选择Java从JDK 5开始引入了增强for循环专门用来遍历数组和集合int[] arr {1, 2, 3, 4, 5}; for (int num : arr) { System.out.println(num); }这个写法简洁不需要管理下标也不容易越界。但它有个致命限制遍历过程中拿不到当前下标。如果你需要用到下标比如修改数组中某个位置的元素就得老老实实回退到普通for循环。// 想修改数组元素必须用普通for循环 for (int i 0; i arr.length; i) { arr[i] arr[i] * 2; } // 增强for循环只能读取不能修改原数组 for (int num : arr) { num num * 2; // 这不会修改数组里的值 }这里涉及一个Java基础概念基本类型在循环变量中传递的是值的副本你改了循环变量原数组纹丝不动。如果遍历的是对象数组通过循环变量修改对象内部状态是有效的因为引用指向同一个对象但如果你重新给循环变量赋值那也不会影响原数组。5.4 死循环的常见成因与排查技巧写循环多了死循环几乎是必然会遇到的。我自己遇到过几种比较典型的情况。第一种是条件永远为真。比如上面提到的忘记写i或者条件写反了。这种问题肉眼看代码就能发现。第二种是循环变量更新逻辑有误。比如在循环体内部不小心给循环变量重新赋值了for (int i 0; i 10; i) { // 其他代码... i 0; // 手滑写了个重置这下永远循环不完 // 其他代码... }第三种是浮点数比较导致的问题。浮点数在内存中不是精确存储的比如0.1在计算机里是一个无限循环的二进制小数。如果你用while (x ! 1.0)这种判断很可能永远无法退出。double x 0; while (x ! 1.0) { x 0.1; System.out.println(x); }这个循环看起来应该执行10次因为0.1加10次正好等于1.0。但因为浮点数精度问题x的值可能永远不会精确等于1.0这个循环就会一直跑下去。排查起来也很费劲因为打印出来的x看起来就是1.0但实际上可能是0.9999999999999999。遇到这种问题解决方案是不要用!来判断浮点数而是用一个很小的误差范围或者干脆用整数循环次数来控制。5.5 流程控制相关的高频面试题汇总结合热搜词里的“java面试题”、“java基础面试题”、“java面试大全及答案”我把流程控制这一块的高频考点整理了一下。虽然流程控制本身不是面试重点但它往往是手写代码题的基础而且有些概念题专坑基础不牢的人。问题一if和switch有什么区别各自适用什么场景答案要点if适合范围判断和复杂逻辑判断比如判断分数区间、判断多个条件组合switch适合等值判断比如判断枚举值、判断一个整数等于几。switch的优点是效率高、结构清晰缺点是只能做等值判断。问题二for循环和增强for循环foreach有什么区别答案要点增强for循环基于迭代器Iterator实现写起来简洁但遍历过程中不能修改集合结构不能增删元素否则会抛ConcurrentModificationException普通for循环可以自由控制下标适合需要修改或删除元素的场景。问题三break和continue在嵌套循环中有什么区别答案要点break跳出当前循环continue跳过本次循环进入下一次。在嵌套循环中两者都只作用于最内层循环。如果需要跳出外层循环可以用带标签的break但更推荐用return抽取方法。问题四while和do-while的区别答案要点while先判断后执行可能一次都不执行do-while先执行后判断至少执行一次。实际使用中如果明确需要先执行一次再判断结果就用do-while。问题五switch支持哪些数据类型答案要点经典switch支持byte、short、char、int四种整数类型其实内在是int以及枚举类型JDK 5、String类型JDK 7。JDK 14的switch表达式还支持Lambda风格的块。特别注意switch不能直接支持long类型这是很多面试官爱挖的坑。5.6 流程控制与后续学习的衔接学完流程控制接下来基本上就是方法、数组、面向对象这些内容了。但流程控制这个基础会在后面到处用到。比如学Java集合的时候遍历List、Map操作Iterator全是循环和条件判断的排列组合。学Java IO的时候读写文件最关键的就是while ((line reader.readLine()) ! null)这种循环模式。学Java并发的时候你可能要处理循环等待、状态判断也都是流程控制的进阶用法。所以Day03这个节点我给你们的建议是不要急着赶进度多写几道循环相关的练习题。九九乘法表、打印三角形、斐波那契数列、判断素数这些经典的入门题每道都老老实实写一遍。这些题目本身不难但能帮你把循环的边界条件、嵌套逻辑、条件判断这些基本功打扎实。6. 一个完整的综合案例简易计算器前面讲了这么多最后咱们用一个综合案例把今天的知识点串起来。写一个控制台版本的简易计算器支持加、减、乘、除四则运算并且能够循环使用直到用户输入特定命令退出。import java.util.Scanner; public class SimpleCalculator { public static void main(String[] args) { Scanner scanner new Scanner(System.in); boolean running true; while (running) { System.out.println( 简易计算器 ); System.out.print(请输入第一个数字); double a scanner.nextDouble(); System.out.print(请输入运算符 - * /); String op scanner.next(); System.out.print(请输入第二个数字); double b scanner.nextDouble(); double result; switch (op) { case - result a b; case - - result a - b; case * - result a * b; case / - { if (b 0) { System.out.println(错误除数不能为0); continue; } result a / b; } default - { System.out.println(错误无效的运算符); continue; } } System.out.println(结果 a op b result); System.out.print(是否继续(y/n)); String choice scanner.next(); if (choice.equalsIgnoreCase(n)) { running false; } } System.out.println(感谢使用再见); scanner.close(); } }这个案例里用到了while循环控制整个程序的运行循环用到了switch做运算符分派用到了continue跳过异常输入情况还用到了if判断退出条件。一个入门阶段的综合案例把今天学的知识都串起来了。特别说一下除数为0的处理。这里用了if (b 0)做判断然后continue跳过本次循环不输出结果直接进入下一次输入。这样处理比用异常处理要简单直观得多。而且b 0这个浮点数比较因为0在浮点数里是精确表示的所以不存在精度问题。7. 流程控制学习的进一步建议最后分享一点我个人在实际教学和面试中总结出来的体会希望能帮你们少走弯路。第一个体会流程控制要多动手画执行轨迹。遇到复杂的嵌套循环或者带跳转语句的代码光靠脑子想很容易出错。找个草稿纸把每个变量的变化过程一步步写下来循环几次、每次变量变成多少、输出什么一清二楚。这个习惯看起来笨但特别管用尤其对初学者来说。第二个体会写循环之前先想清楚结束条件。很多新手拿到需求就开写循环条件做到哪算哪结果代码跑出来要么多循环一次要么少循环一次。我的习惯是动笔之前先把“循环结束的时候循环变量处于什么状态”想明白。比如遍历数组结束的时候下标等于数组长度这就是循环不变式。想清楚这个边界条件就不容易错。第三个体会尽量让循环体保持“什么条件都不满足也能安全运行”的状态。打个比方循环体就像一台机器你要保证即使输入的数据是空的、是极端值、是非法数据机器也不会崩溃。这就是为什么我在案例里加了除数为0的判断、无效运算符的判断。真实开发中你写的代码要面对的用户输入永远比你想象的更离谱。流程控制这块内容说难不难说简单也不简单。语法本身一天就能学会但真正写得好是需要时间和练习堆出来的。建议你们学完这一篇之后把常见的算法题拿过来练手不要看答案自己硬着头皮写。写不出来没关系先把循环结构和分支结构搭起来再一步步调试完善。这个过程才是真正把知识变成能力的过程。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →