
先说说我自己的经历。当年刚开始学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);这很自然因为菜单肯定要展示一遍。但如果你希望“用户没有权限时连菜单都不显示”那就要用whilewhile (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.1x经过叠加后大约是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都写一遍感受一下那个分号、那个先做再判断的顺序比背任何面试题都管用。等你写多了会发现循环结构选哪个心里自然就有数了。