☰
Java while与do-while循环:区别、用法、坑与面试题全解析
2026/10/3 3:55:55 网站建设 项目流程

先说说我自己的经历。当年刚开始学Java的时候,循环结构里最先学会的是for,用到while并不多,一看到do-while只觉得别扭——为什么要先做一遍再去判断?后来在项目里写用户输入校验、写消息队列的消费逻辑,才发现这两个结构几乎是绕不开的。很多人对while和do-while的认知只停留在"语法题"层面,能够在代码里熟练使用、知道何时该用哪一个的人,其实是少数。这篇短文就把while结构和do-while结构掰开揉碎讲一遍,包括它们的执行逻辑、典型坑、面试常考点,以及我实际编码中的选型经验。不管你是刚入门Java的小白,还是准备面试的求职者,都能在里面找到用得上的东西。

1. 先从最常用的while说起

1.1 while的语法骨架和执行顺序

while结构在Java里的标准写法是这样:

while (布尔表达式) { // 循环体 }

它的执行顺序可以用一句话概括:先判断,后执行。程序运行到while这一行时,首先对括号里的布尔表达式求值,如果结果是true,就进入循环体执行一次;执行完之后回到条件判断处,再次判断结果,如果依然是true,就再执行一次……直到某次判断为false,循环立刻终止,跳到循环体后面的代码继续走。

这里有个容易被人忽略的点:布尔表达式的求值结果必须是一个boolean类型,不能是数字、对象引用或者字符串。比如while (1)这种C语言写法在Java里直接编译不过,必须写成while (true)或者while (flag == true)。刚开始学Java的人从C/C++转过来时,经常在这一步踩坑。

另外,如果布尔表达式一开始就是false,那循环体一次都不会执行,这个特性非常重要。后面对比do-while的时候,你会发现这是两个结构最核心的区别。

1.2 从1加到100:一个最直观的示例

大多数教材里都会用"计算1到100的和"来演示while。我也沿用这个例子,但想额外说说循环控制变量的重要性。

public class WhileDemo { public static void main(String[] args) { int i = 1; int sum = 0; while (i <= 100) { sum += i; i++; } System.out.println("1到100的和是:" + sum); // 输出5050 } }

这个例子里,变量i就是循环控制变量,它每轮循环结束后自增一次,从而逐渐逼近不满足条件i <= 100的时刻。如果把i++这行去掉,循环会永远执行下去,因为i始终是1,条件永远为true,最终程序卡死,甚至CPU占用拉满。这是新手最常见的问题。

我见过很多人在写while循环时,把控制变量的更新放在循环体最后一行。这样做没什么问题,但如果你在中间写了continue语句,就要格外小心。关于continue和循环控制变量的配合,我会在后面专门开一节讲,那是真正的坑。

1.3 为什么说while是“先看再走”

可以把while想象成一个门卫:它拦在你进门前,先检查你的证件,证件合格才放你进去。如果第一次检查就不合格,那你连门都迈进不去。

这个“先看再走”的特性,决定了while非常适合处理“前置条件必须满足才执行”的场景。比如从数组里取元素,但如果数组可能为空,你希望直接跳过处理;再比如读取文件时,文件可能不存在,你希望不进入读取逻辑。这些场景用while天然贴合。

int[] data = ...; int index = 0; while (index < data.length && data[index] > 0) { // 只处理数组中连续大于0的元素 index++; }

这种写法就是典型的“先判断条件,满足才行动”。用while代码的意图非常清楚,读代码的人一眼就能明白:只有索引有效并且数值为正数,循环才会继续。

2. do-while到底特殊在哪

2.1 语法和执行顺序

do-while的基本语法是:

do { // 循环体 } while (布尔表达式);

注意,在while (布尔表达式)的末尾,有一个分号;。这个分号代表do-while语句的结束,丢了它编译器会直接报错。很多人第一次写do-while时都会忘记这个分号,包括我自己当年也犯过,属于“语法洁癖”层面的问题,但确实很常见。

执行顺序上,do-while与while正好相反:先执行一遍循环体,再判断布尔表达式。如果表达式为true,就回到循环体开头继续执行下一遍;如果为false,循环结束。这意味着循环体一定会至少执行一次,无论条件是否为false。

2.2 核心特性:至少执行一次

要说do-while和while最大的区别,就是“至少执行一次”这个行为。很多场景正好需要这种语义。最典型的就是用户输入校验。

假设我要写一个程序,要求用户输入一个正整数,如果输入负数、零或者非数字,就反复提示重新输入。用while写是这样:

Scanner scanner = new Scanner(System.in); int number = -1; // 先给一个不合法初值 while (number <= 0) { System.out.print("请输入一个正整数:"); if (scanner.hasNextInt()) { number = scanner.nextInt(); } else { scanner.next(); number = -1; } } System.out.println("你输入的是:" + number);

这代码其实也能跑,但需要人为设置一个明显不合法的初始值-1来“骗”过第一次判断。如果某天有人把初始值改成了1,循环就直接跳过了,这就会引入逻辑bug。

用do-while写,语义就完全贴合场景:

Scanner scanner = new Scanner(System.in); int number; do { System.out.print("请输入一个正整数:"); while (!scanner.hasNextInt()) { scanner.next(); System.out.print("输入不合法,请重新输入:"); } number = scanner.nextInt(); } while (number <= 0); System.out.println("你输入的是:" + number);

先执行一次输入操作,拿到数据后再判断是否合法。无论用户第一次输入什么,程序都会至少执行一次循环体。这种“无论如何都要先做一次,再看结果满不满足要求”的逻辑,用do-while写出来是一种天然的表达。

2.3 结尾那个分号真的不能丢

再强调一次,while (BooleanExpression);后面有分号。如果漏掉,编译的时候会提示“非法开始表达式”之类的错误。有次我同事写了一个do-while,编译不过,找了一圈发现就是分号问题。他当时说:“这不就是个分号吗,也太坑了。”我笑着说:“你要是多写几个do-while,就知道这其实是肌肉记忆。”

建议在编码时尽量使用IDE的自动格式化,或者养成写完do-while立刻检查分号的习惯。从语法上看,do-while是唯一需要以分号结尾的循环结构(for和while都不需要)。

3. 真实项目里该选谁:三种典型场景

3.1 用户输入校验:优先选do-while

这一点在上一节已经展示过了。凡是“先输入,后判断”的场景,do-while都是最贴切的选择。类似的还有:先读取一行配置,再看是否符合格式;先调用一次接口,再看返回码是否成功。只要循环体至少要执行一次,do-while就是最直观的写法。

我自己在写命令行工具时,几乎把所有用户输入拦截逻辑都写成了do-while。不是为了炫技,而是这样写出来的代码,别人读起来不需要去推理“为什么number要初值-1”,直接把业务逻辑摊开了。

3.2 未知次数的后台消费任务:优先选while

消息队列的典型场景是这样的:

while (queue.isNotEmpty()) { Message msg = queue.poll(); handle(msg); }

队列里可能有消息,也可能没有。如果没有消息,就不应该处理任何内容,所以要先判断。这种“有条件才处理”的场景,用while是自然的。如果用do-while,当你第一次进循环时队列就是空的,你还非得先拿一次消息,拿到null再处理,这就很别扭。

类似的也适用于网络请求的重试:你可能希望“最多重试三次,每次间隔两秒”,但前提是第一次请求就失败了,这时候设计一个while (retryCount < 3 && !success)的循环,非常清晰。

3.3 菜单程序的两种写法

一个常见的需求是展示菜单让用户选择。很多人会问:菜单程序用while还是do-while?

我的答案是:都能用,但语义上略有差别。假设菜单必须至少显示一次,然后根据用户输入决定是否退出:

int choice; do { showMenu(); choice = readUserChoice(); } while (choice != 0);

这很自然,因为菜单肯定要展示一遍。但如果你希望“用户没有权限时连菜单都不显示”,那就要用while:

while (user.hasPermission()) { showMenu(); choice = readUserChoice(); if (choice == 0) break; }

所以并不存在绝对的“菜单用哪个”,关键看你希望程序在某种初始状态下是否应该执行循环体。

3.4 两个结构真的可以互相替换吗

从逻辑能力上来说,任何while循环都可以改写成do-while,反之亦然。你完全可以用一个布尔变量来控制“是否第一次执行”来模拟do-while的至少执行一次;也完全可以用条件变量配合break来模拟while。

但代码不只是要给编译器看的,更是给人看的。如果你要用while模拟do-while,写出来的东西通常是:

boolean first = true; while (first || condition) { first = false; // 循环体 }

这种代码读起来很绕,我们通常不会这么写。反过来,用do-while模拟while也不推荐,因为你需要额外保证第一次执行不会造成副作用。结论是:选择一个语义最匹配的结构,而不是总想着“能不能替代”。

4. 这些坑你迟早会碰到:while/do-while的常见雷区

4.1 死循环:条件永远为true

死循环是while相关的第一大坑。最常见的原因就是循环控制变量没有更新,或者条件本身写得有逻辑问题。比如:

int i = 0; while (i < 10) { System.out.println(i); // 忘了 i++ }

这个程序会无限输出0。这种情况下,条件永远为true,循环永远结束不了。写代码时要时刻问自己:这个循环会在什么情况下结束?如果找不到答案,那它多半会成为一个运行半天不退出、CPU占用100%的隐藏炸弹。

如果确实需要无限循环,建议写成while (true),然后在循环体内部通过break来退出。break旁边的注释要写清楚退出条件,比如:

while (true) { Line line = readLine(); if (line == null) { break; // 读到文件末尾时退出 } process(line); }

这种方式比while (i >= 0)这种模糊条件可读性好得多。

4.2 变量作用域:循环体内的变量外面用不了

Java里变量的作用域由花括号决定。while循环体是一个代码块,在循环体内部声明的变量,只能在该代码块内使用。你要是想统计一组数据的最大值、平均值,或者把循环中计算出的结果存下来,就必须把变量声明在循环外面。

举个例子:

int count = 0; while (count < 5) { String line = "第" + count + "行"; count++; } // 这里访问line会编译报错,因为line不在作用域内 System.out.println(line);

正确的做法是把需要留到后面的变量声明在循环外:

String lastLine = ""; int count = 0; while (count < 5) { lastLine = "第" + count + "行"; count++; } System.out.println(lastLine);

这个坑虽然简单,但在处理文件读取时很常见。很多人想把循环里读到的最后一行拿出来用,结果变量定义在循环里,怎么也访问不到,最后折腾半天才发现是作用域的问题。

4.3 continue和循环控制变量:一对阴险的组合

continue在while循环里的行为,比在for里要危险得多。for循环中,continue执行后会跳转到“更新表达式”部分,然后才做条件判断,所以循环控制变量的更新一般不会因为continue被跳过。但在while循环中,continue会直接跳到条件判断处,循环体里从continue到结尾的所有语句都会被跳过,包括你放在末尾的变量更新语句。

看这个例子:

int i = 0; while (i < 10) { if (i == 5) { continue; } System.out.println(i); i++; }

当i等于5的时候,continue会让程序跳过i++,回到条件判断处。此时i还是5,于是又进入if,又continue……无限循环。这个bug相当隐蔽,因为程序运行起来不会崩溃,只是CPU一直占着,等你用调试器去追,才发现i卡在5上。

解决办法,一是把控制变量的更新放在continue之前,比如:

while (i < 10) { if (i == 5) { i++; continue; } System.out.println(i); i++; }

但这样代码重复,容易出错。更推荐的做法是:如果要在循环体里使用continue,优先换成for循环,因为for循环的更新表达式是独立于循环体的,continue不会跳过它。这也是很多风格指南建议“尽量用for代替while”的一个原因。

4.4 浮点数比较导致死循环

还有一类死循环,根源不在逻辑,而在浮点数的精度。比如:

double x = 0.0; while (x != 1.0) { x += 0.1; System.out.println(x); }

表面上看,x从0开始每次加0.1,加到10次应该等于1.0,循环结束。但实际上由于二进制浮点数无法精确表示0.1,x经过叠加后大约是0.9999999999999999,永远不等于1.0,于是循环同样停不下来。

如果要判断浮点数的循环条件,应该用阈值而不是精确相等:

while (Math.abs(target - current) > EPSILON) { current += step; }

其中EPSILON是你允许的误差范围,比如1e-8。这个坑不只在while里有,任何浮点数比较都可能遇到,但while循环因为它天生“次数未知”,更容易被这类问题拖进死循环。

4.5 break只能跳出一层循环

break在单层循环里很好用,但在嵌套循环里就有点“短视”了。默认情况下,break只会终止它所在的那一层循环,外面那层还会继续执行。

比如你要遍历二维数组,找到第一个等于目标值的元素,准备找到后退出所有循环。如果直接写:

int[][] matrix = ...; int target = 88; boolean found = false; for (int i = 0; i < matrix.length; i++) { for (int j = 0; j < matrix[i].length; j++) { if (matrix[i][j] == target) { found = true; break; // 只跳出了内层循环 } } // 外层循环还在继续 }

你会发现break之后,外层循环还会继续跑,程序并不会如预期那样彻底退出。

想打破这种限制,可以用Java的标签语法:

outer: for (int i = 0; i < matrix.length; i++) { for (int j = 0; j < matrix[i].length; j++) { if (matrix[i][j] == target) { found = true; break outer; } } }

标签名可以随便起,但为了避免混淆,一般都叫outer或者能表达业务含义的名字。不过我在实际项目里其实很少用带标签的break,因为它会让代码的控制流跳来跳去,可读性下降。更多时候我会用一个布尔变量作为“是否需要继续循环”的标记,或者把逻辑提取成一个方法,通过return直接返回结果,反而更清晰。

5. 面试官爱问的while与do-while:从语法到底层理解

5.1 一张表看清两者区别

面试里最常问的就是这题:“while和do-while有什么区别?”一个简洁的回答是:

对比项whiledo-while
执行时机先判断后执行先执行后判断
最少执行次数0次至少1次
语法结尾无需分号必须有分号
适用场景前置条件满足才执行无论如何都要先执行一次
典型使用队列消费、条件遍历用户输入校验、菜单循环

但面试官通常还会追问一句:“你实际开发中怎么选?”这时不要只背表格,要结合场景说。我会说,看循环体是否“必须执行一次”:如果不执行也没关系,甚至可能产生副作用,就选while;如果循环体代表一个必须先发生的动作(比如读取输入、展示菜单),那就选do-while。

5.2 while(true)和do{}while(true)能等价吗

很多人会纠结while(true)和do {} while(true)哪个更好。

先看两个写法:

while (true) { // 循环体 }
do { // 循环体 } while (true);

逻辑上,两者都是无限循环,循环体都会无限次执行,只有在break或return时退出。但习惯上,几乎所有人都会写while(true),因为它的语义更直白——无限循环就是“一直判断为真”,用do-while写出的无限循环有点怪,好像在强调“先执行一次”。而事实上无限循环里,第一次执行不执行根本不重要。

我个人的建议是:除非你真的想表达“这个循环体至少执行一次”,否则统一用while(true)。这和性能无关,纯粹从可读性出发。

5.3 字节码层面的差异:判断的位置不同罢了

继续往下聊,面试官可能会问:“它们在底层实现上有什么区别?”其实它们编译后的字节码结构很相似,核心都是条件跳转指令(if_icmpgt、goto等),主要区别在于跳转的位置。

一个while循环编译后大致是:

  • 先做条件判断
  • 不满足就跳到循环体外面的地址
  • 满足就执行循环体
  • 执行完跳回条件判断处

一个do-while循环编译后则是:

  • 先执行循环体
  • 循环体执行完做条件判断
  • 满足则跳回循环体开头
  • 不满足则结束

所以从底层指令上说,do-while的代码路径比while少了一次“初始条件判断”。不过现代JVM的JIT编译器会把循环优化得很聪明,这个差异在性能上通常可以忽略不计。真正有意义的是它解释了为什么do-while能保证至少执行一次——因为第一次执行时根本没有任何判断,直接进入循环体。

如果你有兴趣,可以写两个简单方法,然后用javap -c命令查看字节码。你会发现while版的方法中,条件判断指令在前,循环体在后;do-while版则是循环体在前,条件判断在后面。这算是帮助你理解语法本质的一个小实验。

5.4 循环结构的选择心法:别硬凑,选语义最贴近的

面试里最后一个高频追问是:“既然for能做的事while都能做,为什么还要用for?”

答案是:代码不仅要执行正确,还要让别人一眼读懂意图。循环次数明确、有步进更新时,用for最清晰;循环次数不确定、全靠条件终止时,用while;至少要执行一次的业务动作,用do-while。选错了不是不能跑,只是读者需要多花一点时间去理解你的真实意图。

举几个简单的选型参考:

  • 遍历数组、集合,知道要循环多少次,用for
  • 读取文件直到EOF,循环次数未知,用while
  • 先让用户输入一次,不合格再重来,用do-while

把这些原则记熟,面试时答得出来,写代码时也少一点纠结。

最后说点我的实际习惯

我平时写Java代码,do-while用得不算特别多,但只要碰上“用户交互”和“读取校验”这类场景,几乎就固定用它。它替我解决了一个很现实的问题:不用靠一个会发生变化的初始值来控制流程,代码少了一些假变量,也就少了一些隐性bug。

如果你现在正在学Java,我建议你亲手写几个小程序练一练:一个猜数字游戏、一个输入密码判断、一个菜单循环。不要只是看课,把while和do-while都写一遍,感受一下那个分号、那个先做再判断的顺序,比背任何面试题都管用。等你写多了会发现,循环结构选哪个,心里自然就有数了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询