在Java的所有语法里,流程控制是我建议每个初学者第一个彻底吃透的知识点。它不像面向对象那样需要反复理解抽象概念,也不像集合框架那样需要背大量API,但几乎所有代码的执行逻辑都离不开它。哪怕你以后写的是业务代码、算法题,甚至看框架源码,第一步要做的都是先把流程捋顺。所谓Java流程控制,简单说就是让程序按照你设定的条件去选择走哪条路、重复做哪些事、什么时候停下来。本篇我会从整体分类、分支语句、循环语句、跳转控制、实战案例、面试排坑六个方面,把Java流程控制讲透,不仅说语法,更聊背后的设计思路和我在实际开发里踩过的坑。
我见过不少准备Java面试的朋友,一上来就背八股文,什么HashMap原理、JVM内存模型,结果问一句“break和continue到底什么区别,写个双重循环怎么跳出外层”,反而卡壳。就是因为流程控制太基础,很多人以为会写for循环就够了,没有真正理解控制流思维。这篇内容适合Java入门阶段的学习者,也适合正在刷Java基础面试题的人,我会把常用的写法、容易出错的点、面试官爱问的变形题都放进来,你可以直接照着练习。
1. 流程控制的整体认知与设计思路
1.1 为什么流程控制是所有Java代码的骨架
代码本身是自上而下逐行执行的,这是计算机最底层的执行方式。但现实问题里,你不可能永远只写顺序步骤。比如你写一个登录功能,用户输入密码后,你得判断密码对不对;不对的话要提示重试,重试几次后要锁定账号;试成功了要跳转到首页,首页要根据用户角色展示不同菜单。这一连串“判断 + 重复 + 跳转”,如果只靠顺序执行一行行写下来,代码会变成一团完全无法维护的乱麻。
流程控制就是用来解决这个问题的语法工具。它分为三大类:
- 顺序结构:程序默认的执行方式,从上到下依次执行每一条语句。这个不用额外写代码,Java天然就是顺序执行的。
- 分支结构:根据条件判断选择执行不同代码块,核心关键词是 if、else、switch。
- 循环结构:在条件满足时重复执行某段代码,核心关键词是 for、while、do-while。
从底层看,分支和循环最终都会被编译器转换成字节码里的跳转指令,但你在写Java时完全不需要关心底层跳转,只需要掌握这三种结构的组合方式。我在带新人时经常打一个比方:写代码就像做一道菜,顺序结构是“按菜谱步骤一步步来”,分支结构是“尝一口觉得咸了就少放盐,淡了就补一点”,循环结构是“一直搅拌到蛋液完全均匀为止”。流程控制就是把这三件事用语法清晰地表达出来。
很多人忽略一个问题:流程控制其实是算法的基础。所谓算法,本质就是“有限步骤解决问题”,而这些步骤的组织方式几乎离不开分支和循环。你去刷LeetCode、写冒泡排序、实现二分查找,核心框架都是循环套分支。所以流程控制不是学完就扔的基础语法,它是伴随整个编程生涯的底层思维。
1.2 三种流程控制结构的思维模型
我在实际写代码时,会先在脑子里用一套固定的思维模型来处理逻辑问题,分享出来供你参考。
第一,顺序结构是默认路径,不需要刻意设计,但要注意代码顺序是否合理。比如你要使用一个变量,必须先声明再使用;你要调用一个对象的方法,必须先初始化对象。这些“必须先做什么”的意识,本质上就是顺序结构的体现。
第二,分支结构要关注“条件是什么”和“条件不满足时走哪条路”。很多人写分支只写了if的情况,忘了写else,结果条件不满足时程序就什么都不做,或者走了错误逻辑。我的习惯是:只要某个分支不是为了“忽略异常情况”,就一定要写else,把兜底逻辑显式表达出来。哪怕else里只写一行注释说明“这里不需要处理”,也比空着强,因为注释告诉后来的维护者你是有意忽略的。
第三,循环结构要关注“循环什么时候开始、什么时候结束、每次循环怎么变化”。用专业一点的话说,就是初始化、条件判断、迭代步进三要素。我用一个简单的公式来帮助记忆:循环次数已知用for,循环次数未知但条件清晰用while,至少执行一次用do-while。这三个选择标准基本覆盖了90%以上的场景。
流程控制的设计有一种“先画图再写码”的做法:遇到逻辑复杂的需求,先在纸上画一个简易流程图,用箭头标出判断节点和循环回路,再转成代码。我早期写复杂嵌套时经常靠这个办法理清思路,比直接上手敲代码效率高很多。
2. 分支结构:if-else与switch的实战要点
2.1 if-else的写法与条件判断的坑
if-else是Java里最基础的分支语句。基本写法有三种:单独的if、if-else、if-else if-else。其中else if并不是Java的关键字,它本质上是“else { if (...) { ... } }”的简写,只不过语法上省略了大括号。
很多初学者在写if时有一个很不好的习惯:大括号能省则省。Java语法允许if后面不带大括号,此时它只控制紧随其后的一条语句。比如下面这段代码:
if (score >= 60) System.out.println("及格"); System.out.println("成绩已登记");第二行输出会被执行吗?会。因为在Java眼里,if只管到第一个分号为止,后面的输出语句是顺序结构的一部分,与if无关。这种问题在缩进混乱时非常隐蔽,排查起来相当痛苦。我的建议是永远不要省大括号,即使if分支只有一行。这不是代码风格洁癖,而是为了降低出错概率。
条件判断里容易踩的坑主要有三类。
第一类是赋值符号和相等判断混淆。对于int类型,如果你写了if (a = 1),编译器会直接报错,因为Java要求if括号里必须是布尔值,a = 1是赋值表达式,返回的是int。但如果变量本身是boolean类型,比如if (isOk = true),这句是能编译通过的,而且永远为true,原本想判断isOk是否为真,结果变成了给它赋值后再判断。这类bug极难发现,因为代码看起来很合理。我个人的防御性写法是把常量写在等号左侧,也就是if (true == isOk),如果少打了一个等号,写成if (true = isOk),编译器就会立刻报错。
第二类是字符串比较。判断字符串是否相等,不能直接写if (str == "abc"),因为==比较的是两个对象的内存地址,而字符串内容相等需要调用equals方法。这是Java面试八股里的高频题,也是实际业务里出现频率极高的bug。正确的写法是if ("abc".equals(str)),把常量放在前面还有一个好处:即使str为null也不会抛出空指针异常。
第三类是浮点数比较。比如判断浮点数是否等于0,直接if (f == 0.0)是很危险的,因为浮点数的精度问题可能导致一个理论上等于0的值实际存储为0.00000000001。正确做法是判断它与目标值的差是否小于一个很小的精度阈值。
下面是一个比较典型的成绩等级判断示例:
int score = 85; if (score >= 90) { System.out.println("优秀"); } else if (score >= 80) { System.out.println("良好"); } else if (score >= 70) { System.out.println("中等"); } else if (score >= 60) { System.out.println("及格"); } else { System.out.println("不及格"); }这里有一个隐藏的逻辑点值得注意:分支判断从上往下执行,一旦某个条件满足,后面的else if就不会再判断。所以分数条件的顺序是有讲究的,必须从大到小写,如果先写score >= 60,那么85分就会直接输出“及格”,达不到你想要的效果。这就是流程控制里的“顺序影响结果”。
2.2 switch语句与switch表达式
switch适合做离散值的等值判断。比如根据用户输入的数字执行不同操作,或者根据枚举类型走不同分支,用switch比用一长串if-else直观得多。
传统switch从Java 7开始支持int、char、String、枚举类型。经典写法长这样:
int day = 3; String dayName; switch (day) { case 1: dayName = "星期一"; break; case 2: dayName = "星期二"; break; case 3: dayName = "星期三"; break; default: dayName = "未知"; break; }这里最核心的坑是switch穿透。如果某个case后面漏写了break,程序会继续执行下一个case的代码,直到遇到break或switch结束。有些初学者会问:为什么Java要设计成必须手动写break,而不是默认每个case自动断开?原因是Java早期为了兼容C语言的语法习惯,保留了这种设计。也正因为这个特性,在一些场景下可以故意使用穿透来简化代码,比如多个case共享同一段逻辑:
switch (month) { case 1: case 3: case 5: case 7: case 8: case 10: case 12: System.out.println("31天"); break; case 2: System.out.println("28天或29天"); break; default: System.out.println("30天"); break; }这种写法在实际开发中是允许的,但一定要加上明确的注释,表明是有意让多个case共用逻辑,否则后面维护的人很容易误以为是漏写了break。
从Java 14开始,switch升级成了switch表达式,支持箭头语法,写法简洁很多:
String dayName = switch (day) { case 1 -> "星期一"; case 2 -> "星期二"; case 3 -> "星期三"; default -> "未知"; };箭头语法有两个特点:一是每个分支默认自带break效果,不需要也不能写break,写了反而编译报错;二是switch可以作为表达式直接给变量赋值,配合yield关键字可以在分支里做更复杂的计算。如果你用的是Java 17或更新的版本,我建议优先使用箭头语法,代码可读性会提升不少。
2.3 分支结构选型建议:什么时候用if-else,什么时候用switch
面试和实际开发里经常会遇到分支选型的问题。我总结了一套自己的判断标准。
如果条件判断基于范围,比如分数是多少区间、年龄多大、价格在什么区间,优先用if-else。因为switch的case只能匹配具体的离散值,没法表达“大于等于90”这种范围概念。
如果条件是基于同一个变量判断它是否等于几个离散值,比如根据状态码1、2、3、4走不同逻辑,用switch更合适。代码结构更清晰,一眼就能看出所有可能的值。
如果条件涉及多个变量的组合,比如“年龄大于18且城市为杭州,或者会员等级为VIP”,只能用if-else配合逻辑运算符实现,switch无法处理。
还有一个经验之谈:当同一变量的离散判断超过三个时,我倾向于用switch而不是if-else。超过六个时,甚至可以进一步考虑用Map或枚举来消除switch,让代码更符合开闭原则。当然这对新手来说有点超前,但至少心里要有这个概念:switch只是手段,不是目的。
3. 循环结构:for、while、do-while的选型逻辑
3.1 for循环的组成与原理解读
for循环是Java里最常用的循环结构,它的语法看起来简单,但执行顺序很容易搞混。基本结构如下:
for (初始化语句; 循环条件; 迭代语句) { 循环体; }执行顺序非常关键:第一步执行初始化语句,且只执行一次;第二步判断循环条件是否为true,如果为false则整个循环结束;第三步执行循环体;第四步执行迭代语句;然后回到第二步继续判断。也就是说,循环体实际上是在“条件判断”和“迭代更新”之间执行的。
一个最简单的累加求和示例:
int sum = 0; for (int i = 1; i <= 100; i++) { sum += i; } System.out.println(sum); // 5050这段代码的执行过程是:先声明i为1,然后判断i <= 100成立,执行sum加1,接着i自增为2,再判断i <= 100仍然成立,继续执行……直到i变成101,条件不满足,循环终止。理解这个执行顺序对排查循环边界问题特别重要。
for循环的三个部分都可以省略,但两个分号不能少。比如for (;;)就是一个死循环,等同于while(true)。初始化部分可以同时声明多个变量,用逗号分隔,比如for (int i = 0, j = 10; i < j; i++, j--)。迭代语句也可以是任意语句,不一定非要是自增,常见的有i += 2、i--等。
这里要提醒一个作用域问题:在for循环初始化部分声明的变量,作用域仅限于这个for循环内部。循环结束后,i就无法访问了。如果你需要在循环结束后还使用i的值,必须在循环外面先声明变量。这个细节在面试里偶尔会被考到。
3.2 while与do-while的区别
while循环适用于“循环次数不确定,但条件清晰”的场景。它的特点是先判断条件,再执行循环体,所以如果条件一开始就不满足,循环体一次都不会执行。
int count = 0; while (count < 5) { System.out.println(count); count++; }do-while和while唯一的区别是:先执行一次循环体,再判断条件。所以do-while至少会执行一次。语法上特别注意结尾要有一个分号:
int count = 0; do { System.out.println(count); count++; } while (count < 5);什么时候用do-while?最典型的场景是用户输入验证。比如一个控制台菜单程序,需要先显示菜单让用户输入,如果输入不合法就重新输入。这个场景天然满足“至少执行一次”,用do-while最贴合语义,不需要额外设置一个初始标志位。我见过很多新手用while实现同样的功能,写法是先把第一次输入写在循环外面,循环里再写一次输入,代码重复很严重。如果换成do-while,输入逻辑只需要写一遍。
实际开发中,do-while的使用频率比while低不少,但它是面试里“区分会不会”的一个小考点,语法细节必须记住。
3.3 增强for循环与集合遍历注意点
增强for循环,也叫for-each,是遍历数组和集合最简洁的写法。它本质上是Java编译器提供的一种语法糖,底层对于数组使用普通for实现,对于Collection集合使用Iterator迭代器实现。
int[] arr = {1, 2, 3, 4, 5}; for (int num : arr) { System.out.println(num); } List<String> list = new ArrayList<>(); list.add("Java"); list.add("流程控制"); for (String s : list) { System.out.println(s); }增强for虽然方便,但有两个限制必须记住。
第一,遍历过程中不能修改集合的结构。如果在增强for循环里调用list.add或者list.remove,会抛出ConcurrentModificationException异常。这是因为Iterator在迭代时会维护一个modCount计数,检测到集合被修改就会快速失败。正确的删除方式是使用Iterator自带的remove方法,或者用普通for循环倒序遍历。
第二,增强for里的循环变量是副本。对于基本类型数组,修改循环变量不会影响原数组;对于对象数组或List,修改对象的属性是会生效的,因为循环变量持有的是对象的引用,但重新赋值循环变量本身不会影响原集合中的引用。这个区别初学者常常混淆,面试时也爱挖这个坑。
4. break、continue与return:跳转控制实战
4.1 break、continue、return各自的定位
跳转控制是流程控制里最容易混的一部分,三种关键字功能完全不同,适用场景也差异很大。我直接用一张表格说明核心区别:
| 关键字 | 作用范围 | 行为 | 典型场景 |
|---|---|---|---|
| break | 循环或switch | 立即结束当前整个循环或switch | 找到目标就退出,不再继续查找 |
| continue | 循环 | 跳过本次循环剩余代码,直接进入下一次迭代 | 跳过不符合条件的数据,继续处理 |
| return | 方法 | 结束整个方法,返回调用处 | 条件不满足时提前结束方法 |
用生活例子来理解:你在排队打饭,break相当于你突然不想吃了,直接离开队伍;continue相当于你发现前面几个人插队,跳过他们,继续站在原地等下一个;return相当于你接到电话说家里有事,直接跑回去不再排队了。
代码层面的表现再看一下:
for (int i = 1; i <= 10; i++) { if (i == 5) { break; } System.out.print(i + " "); } // 输出:1 2 3 4如果是continue:
for (int i = 1; i <= 10; i++) { if (i == 5) { continue; } System.out.print(i + " "); } // 输出:1 2 3 4 6 7 8 9 10这里有一个细节:continue跳过的不是“下一次循环”,而是“本次循环中continue之后的所有代码”,循环本身会继续执行下一次迭代。不少初学者把continue理解成“跳到循环开头”,这个理解在for循环里容易造成偏差,因为for循环的迭代语句(比如i++)即使在continue之后也会执行。但在while循环里,如果continue写在循环体末尾还好,如果写在循环体中间,且迭代变量的更新也在continue之后,就可能造成无限循环。这是必须警惕的。
4.2 带标签的跳转与内层循环跳出
Java中break和continue默认作用于最内层循环。如果你在双重循环里想一次性跳出外层循环,break是做不到的。这个问题有两种解决思路。
第一种是使用标签(label)。Java支持给循环加标签,然后用break 标签名或continue 标签名跳出或跳过指定循环:
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); } }它的效果是当j等于2时,直接终止整个外层循环。continue outer的效果则是跳过外层循环当前这次迭代,继续执行外层循环的下一次迭代。
第二种更推荐的做法是把循环逻辑封装成一个独立方法,用return返回值来控制跳出:
public boolean findElement(int[][] matrix, int target) { for (int i = 0; i < matrix.length; i++) { for (int j = 0; j < matrix[i].length; j++) { if (matrix[i][j] == target) { return true; } } } return false; }这样既走出了循环,又顺便把结果传给了调用方,代码可读性比标签跳转好很多。我个人在代码审查时看到标签跳转,一般会建议同事改成方法抽取的方式。标签不是不能用,但能用return解决的逻辑尽量用return。
4.3 实战中的死循环与退出策略
说到死循环,很多人第一反应是“程序卡死了”。但在实际开发中,死循环是一种非常常见的合法写法,关键在于循环体内要有明确的中断条件。典型场景包括:服务器监听客户端请求、游戏引擎的主循环、控制台交互菜单。
一个最典型的例子是控制台问答程序,直到用户输入“exit”才退出:
Scanner scanner = new Scanner(System.in); while (true) { System.out.print("请输入指令:"); String input = scanner.nextLine(); if ("exit".equals(input)) { System.out.println("程序退出"); break; } System.out.println("你输入的是:" + input); }这种写法在while条件里直接写死true,退出完全依赖循环体内的break。好处是逻辑集中,可读性好;坏处是如果写代码的人忘了加退出条件,或者退出条件被写在一个永远到不了的分支里,程序就会真的卡死。
如果死循环里没有阻塞操作,比如没有读取用户输入、没有网络等待,而是纯粹的空转,CPU占用会直接拉满。这在写一些轮询程序时需要特别注意。一个常见的做法是在循环体内加Thread.sleep让线程休息几毫秒:
while (true) { checkStatus(); Thread.sleep(1000); }这样每秒只执行一次检查,CPU占用非常低。注意要处理InterruptedException异常,或者用try-catch包起来。
5. 经典案例:用流程控制解决真实问题
5.1 案例一:统计字符串中字母、数字、空格出现次数
这个题目是很多Java基础面试题的常客,同时也是流程控制综合运用的一个典型练习。它考察的是循环结构加分支结构的配合,以及字符处理的细节。
假设输入字符串"Hello Java 2025!",要求统计其中字母、数字、空格分别出现了多少次。代码可以这样写:
String str = "Hello Java 2025!"; int letterCount = 0; int digitCount = 0; int spaceCount = 0; int otherCount = 0; for (int i = 0; i < str.length(); i++) { char c = str.charAt(i); if (Character.isLetter(c)) { letterCount++; } else if (Character.isDigit(c)) { digitCount++; } else if (Character.isWhitespace(c)) { spaceCount++; } else { otherCount++; } } System.out.println("字母:" + letterCount); System.out.println("数字:" + digitCount); System.out.println("空格:" + spaceCount); System.out.println("其他:" + otherCount);这个题目的关键点有几个。一是用charAt方法逐个取出字符,这考察对String底层是按char数组存储的理解。二是Character.isLetter、isDigit、isWhitespace这几个方法,比直接比较字符范围更规范,比如isLetter能识别中文字符,而用字符范围判断只能识别英文字母。三是分支的语法顺序,else if之间互斥,不会重复计数。
如果你把整个逻辑封装成一个方法,返回一个int数组,就可以复用到其他项目里。这也是从流程控制过渡到方法设计的一个很好练习。
5.2 案例二:判断质数并优化
判断一个数是否为质数是循环结构的经典练习。基础版本从2遍历到n-1,每个数都判断能否整除:
boolean isPrime(int n) { if (n < 2) { return false; } for (int i = 2; i < n; i++) { if (n % i == 0) { return false; } } return true; }但这个版本效率不高。如果n是10万,循环就要执行近10万次。数学上可以证明,一个合数必定有一个小于等于它平方根的因子,所以只需要判断到根号n就够了:
boolean isPrimeOptimized(int n) { if (n < 2) { return false; } if (n == 2) { return true; } if (n % 2 == 0) { return false; } for (int i = 3; i * i <= n; i += 2) { if (n % i == 0) { return false; } } return true; }优化点有两个:一是循环范围缩小到平方根,二是步长从1改成2,跳过所有偶数因子,效率提升一倍。这个题目的价值在于体现“算法优化的意识”,面试官很看重面试者是否能在完成基础功能后进一步思考性能。循环条件里的边界i * i <= n要特别注意,如果写成i <= n / i会更安全,因为i*i可能溢出int范围,虽然在这个场景里n不会太大,但养成边界意识是好的。
5.3 案例三:用嵌套循环打印九九乘法表
九九乘法表是练习嵌套循环的经典题目,几乎每个Java初学者都写过,但真正理解内层循环和外层循环关系的人不多。先看代码:
for (int i = 1; i <= 9; i++) { for (int j = 1; j <= i; j++) { System.out.print(j + "×" + i + "=" + (i * j) + "\t"); } System.out.println(); }外层循环控制行数,内层循环控制每一行打印多少个式子。这里最重要的是内层循环的终止条件是j <= i,也就是说每一行的列数等于当前的行号。第一行i=1只打印一个式子,第二行i=2打印两个式子,以此类推。
很多新手在这里犯的错误是写成j <= 9,这样每一行都打印9个式子,输出结果就是一个矩形而不是三角形。这个问题的根源在于没有建立起“外层循环变量可以控制内层循环范围”的意识。
如果你能把九九乘法表改成倒三角、平行四边形,或者打印菱形,嵌套循环的掌控基本就入门了。我的经验是:动手写之前,先在纸上列出几行,找出行数、列数、空格数之间的数学关系,再转成代码。这种“列公式再翻译”的思路,在以后写复杂算法时非常有用。
6. 常见问题排查与面试考点拆解
6.1 新手最容易踩的几个坑
把流程控制相关的常见问题整理成一张速查表,方便你在写代码时自查。
| 问题现场 | 根本原因 | 解决方案 |
|---|---|---|
| if分支执行结果不符合预期 | 大括号省略,if只管到第一个分号 | 永远写大括号 |
if (str == "abc")判断内容总是不对 | 字符串==比较的是地址,不是内容 | 改用"abc".equals(str) |
| switch只执行了部分case,后面的也执行了 | 分支漏写break,造成穿透 | 检查每个case末尾的break |
| 循环次数比预期多一或少一 | 边界条件写错,<和<=没分清 | 用具体数代入模拟执行一遍 |
| do-while循环编译报错,提示少分号 | do-while结尾必须有分号 | 在while条件后补分号 |
| while循环卡死,程序不退出 | 循环条件始终为true,且没有break | 检查循环体内是否有更新循环变量的语句 |
| foreach遍历list时删除元素抛异常 | 增强for底层是Iterator,不能结构修改集合 | 用Iterator.remove或普通for倒序遍历 |
这些坑我几乎每个月都会在报错群或者代码审查里看到。尤其是字符串用==比较这个问题,Java面试八股里反复考,实际项目里反复犯错,简直可以算Java第一坑。
第7条再补充一个容易忽略的:while循环里如果更新循环变量的语句在continue之后,continue会直接跳过更新语句,导致循环变量永远不变化,进而死循环。比如:
int i = 0; while (i < 10) { if (i == 5) { continue; } System.out.println(i); i++; }当i等于5时,continue跳到循环条件判断,i还是5,继续进入循环体,又遇到continue,无限循环。这种问题在排查时非常隐蔽,因为代码看起来没有任何异常。解决办法是把循环变量更新放在continue之前,或者用for循环替代。
6.2 面试常考的流程控制题目
除了前文提到的统计字符、判断质数、九九乘法表之外,还有几个经典题目值得练手。一个是冒泡排序,它是最典型的“双重循环 + 分支交换”组合。基础版代码:
int[] arr = {5, 2, 8, 1, 9}; for (int i = 0; i < arr.length - 1; i++) { for (int j = 0; j < arr.length - 1 - i; j++) { if (arr[j] > arr[j + 1]) { int temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; } } }内层循环的arr.length - 1 - i是冒泡排序的精髓:每一轮排序都会把当前最大值放到末尾,所以下一轮就不需要再比较已经排好的部分。如果你是手写代码,很容易把这部分写成arr.length - 1,功能没错但效率变低,面试官会追问“这个-i的意义是什么”,能答上来才算真正理解。
另一个高频题是打印菱形或者水仙花数。打印菱形的核心是嵌套循环加空格控制,水仙花数则是分支与取余运算的结合。这些题目我都建议你在IDE里手敲一遍,而不是只看答案。流程控制是动手技能,看十遍不如写一遍。
面试里还有一种变形题:给定一个循环,让你判断输出结果。比如交换break和continue的位置,或者把判断条件从i < 5改成i <= 5,问输出有什么变化。这种题考察的是对执行流程的精确理解,没有捷径,只能把循环执行顺序背熟并反复推演。
6.3 实战心得与编码习惯建议
写流程控制到现在,我自己沉淀了一些心得。
第一,写循环之前先问自己三个问题:循环次数是否已知?循环条件是什么?循环结束后需要留下哪些数据?这三个问题答案明确了,代码基本不会走偏。比如遍历数组,次数已知用for;读取用户输入直到合法,次数未知用while;输出至少执行一次的菜单,用do-while。
第二,嵌套控制在两层以内通常是清晰的,超过两层就需要警惕。三层以上的嵌套循环或者多层if嵌套,会让代码可读性急剧下降。我的习惯是把内层逻辑抽成方法,让外层结构保持扁平。你以后去读Spring、MyBatis这种框架源码时也会发现,它们的流程控制层次都很浅,复杂逻辑靠方法调用来组织。
第三,调试时善用断点和输出。找循环bug时,我在循环体里临时加一行System.out.println("i=" + i + ", j=" + j),把每一步的循环变量打印出来,配合纸笔模拟,很快就能定位问题。现在的IDE支持条件断点,可以直接在断点上设置条件比如i等于5时暂停,比通过日志排查高效得多。
第四,写代码时要看循环边界。我刚工作那年,写一个分页查询,循环条件用了i <= totalPage,结果多查了一页空数据。就是因为没有仔细分析边界。后来我形成习惯:写出循环后,拿最小值和最大值手动代入跑一遍,确认循环次数符合预期。这个习惯帮我避掉了不少隐性bug。
最后再分享一个小技巧:如果你在面试时需要手写循环逻辑,先写出最直观的版本,确保功能正确,再提优化。很多面试者上来就写一大堆优化,结果边界条件没处理好,反而丢分。流程控制的本质是逻辑清晰,优化是锦上添花,先做对,再做好。