ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java循环结构详解:while与do-while的区别、用法与面试考点

Java循环结构详解:while与do-while的区别、用法与面试考点 说到Java里的循环结构很多人下意识先想到for循环然后才是while和do-while。但实际在工作里while和do-while用得一点不比for少尤其是在处理“不确定要循环多少次”的场景——读取用户输入直到满足条件、轮询某个状态是否就绪、写菜单交互、做协议解析这些都是while家族的拿手好戏。而且面试里“while和do-while的区别”这个经典问题每年不知道放倒多少人。这篇我打算把这两个循环结构彻底拆开讲一遍从语法到执行流程从典型场景到实战坑点再到面试怎么答通通捋清楚。你要是刚学Java可以当入门教程看要是准备面试可以直接跳到后面看考点总结要是有两年经验但一直用得稀里糊涂我建议你把“为什么有时候非要用do-while”那几段多看两遍应该会有收获。1. while和do-while的本质区别一个先问再做一个先做再问先记住一句话while是先判断条件再执行循环体do-while是先执行循环体再判断条件。这个顺序差异导致了一个极其重要的结果——do-while的循环体至少会执行一次而while可能一次都不执行。拿生活里的例子类比。while像是你出门前看天气预报如果下雨条件不满足那你就不出门了一次都出不去。do-while则像你直接出门淋了一场雨回来之后才想“外面下雨了明天别出门了”——事情已经做过了后悔也晚了。这个类比虽然粗糙但把两者的核心差异抓住了do-while没有“零次执行”的可能。再看代码层面的执行流程。一个标准的while结构长这样初始化; while (条件判断) { 循环体; 更新循环变量; }程序执行到while时先计算条件表达式的值。为true进循环体为false直接跳过整个循环继续往下走。循环体执行完一遍之后流程再回到条件判断重复这个过程直到条件为false。do-while的流程则完全不同初始化; do { 循环体; 更新循环变量; } while (条件判断);程序先执行一遍循环体然后才去判断条件。条件为true回来再执行一遍条件为false跳出循环。注意这里有个语法细节do-while的while (条件)后面必须跟一个分号这是很多新手第一次写do-while时最容易漏掉的东西漏了分号直接编译报错。从“先判断还是先执行”这个根本差异出发就能理解为什么有些场景非用do-while不可。比如你写一个用户交互界面菜单至少要显示一次吧你写一个密码输入验证至少要给用户一次输入机会吧这种“无条件先跑一次”的需求用while硬写也能实现但代码会变得别扭——你得在while之前先手动执行一遍循环体这就等于把循环体复制了一份维护起来很麻烦。2. while循环的完整解析语法、执行过程与典型场景2.1 while循环的三个关键组成部分一个合格的while循环一定包含三个部分初始化、条件判断、循环变量更新。这三个部分缺一个轻则逻辑错误重则死循环。// 示例计算1到100的和 int sum 0; // 初始化 int i 1; // 初始化循环变量 while (i 100) { // 条件判断 sum i; // 循环体 i; // 循环变量更新 } System.out.println(1到100的和是 sum);这里i就是循环变量它每执行一次循环体就自增1。i 100是循环继续的条件当i增长到101时条件为false循环结束。sum是累加器负责把每次循环的i值累加起来。很多人写while循环时容易把循环变量更新漏掉。比如上面这个例子如果把i删掉i永远等于1i 100永远为true程序就会无限循环下去直到内存耗尽或者任务管理器帮你把它结束掉。在开发环境里跑死循环还好最多卡住一个进程在生产环境里出了死循环CPU直接打满整个服务可能都被拖垮这个后果很严重。2.2 while的经典应用场景读取用户输入直到退出while最常见的实际用途之一就是“不断读取输入直到用户发出退出指令”。因为输入什么时候结束你事先不知道只能一边读一边判断这与while的语义天然匹配。Scanner scanner new Scanner(System.in); String input ; while (!q.equals(input)) { System.out.println(上一次输入的内容 input); System.out.print(请输入内容输入q退出); input scanner.nextLine(); } System.out.println(已退出);注意这里我把q.equals(input)而不是input.equals(q)。原因很简单如果input是nullinput.equals(q)会抛出空指针异常而q.equals(input)只是返回false程序还能正常运行。很多NullPointerException就是这么躲过去的习惯用常量在前面的写法能避免一类很隐晦的运行时错误。这个模式在真实项目中太常见了。服务端的命令行管理工具、自动化脚本里的交互式配置向导、数据处理管道里的逐行扫描都是同一个套路。2.3 while配合break实现“先处理后判断”的半循环结构实际的业务逻辑里有一种场景比单纯的while要刁钻一点你得先读数据、处理数据处理完发现需要退出再判断条件。这种“循环体中间才知道要不要结束”的情况用do-while套不上用单纯while也别扭常见做法是while配合break。Scanner scanner new Scanner(System.in); while (true) { String line scanner.nextLine(); // 这里做一些处理 System.out.println(处理结果 line.trim()); if (exit.equals(line)) { break; } }我自己管这种写法叫“半开半闭循环”——while (true)表示这个循环没有一个天然的出口出口被刻意放在循环体中间。这种结构适合那种“进来就要干活干完才决定要不要继续”的场景。很多初学者看到while (true)就紧张觉得是死循环但配合break之后它其实是非常可控的。写这种循环时要注意两点第一break的条件一定要能在可预期的输入中满足否则就是真的死循环第二循环体内一旦有多层嵌套比如循环里还有ifif里还有内层循环break只能跳出最近的那一层容易产生“怎么没跳出去”的困惑。2.4 while的边界条件设计为什么老程序员对和这么敏感写while循环时边界条件是出错的高发区。我见过最多的问题就出在i n还是i n、i 0还是i 0这类辨析上。// 从10倒数到1 int i 10; while (i 1) { System.out.println(i); i--; }如果条件写成i 1循环会在i等于1时结束数字1就不会被打印出来这就是经典的“差一错误”。调试这种问题很折磨人因为代码编译没问题运行也不报错只是结果差一个数。我的经验是在写循环之前先在纸上写出循环变量的几个关键值。比如要遍历1到10那就写出来i1时该进去i10时该进去i11时不该进去。把这几个值套到条件表达式里看结果是否和预期一致。这个习惯花不了30秒但能省下不少调试时间。边界条件暴露的往往是对问题域理解不够清晰有些人的本意就是遍历到n-1有些人的本意是遍历到n写代码的人和看代码的人如果都对“这个区间是开还是闭”没有统一认识bug就埋下了。3. do-while循环的完整解析先执行后判断的独特价值3.1 do-while的语法细节和容易踩的坑do-while长这样初始化; do { 循环体; 更新循环变量; } while (条件判断);语法虽然简单但三个坑必须记住第1while (条件)后面的分号不能丢丢了直接编译错误第2循环体哪怕只有一行代码大括号也建议写上不写容易串逻辑第3条件判断里的变量一定要在循环体内更新否则就会死循环。还是那句话do-while至少执行一次所以它的死循环风险比while更大——一旦条件永远为true这个循环会从第一次执行开始就无限转圈。你可能注意到do-while的结构里“初始化”只负责给变量定一个初始值条件判断要在循环体跑完之后才进行。所以哪怕初始状态下条件已经不满足了循环体也会硬着头皮执行一次。这就是do-while和while唯一的、也是最核心的差异。3.2 经典场景一菜单选择交互做命令行工具或者简易管理系统时菜单交互是do-while的最佳实践。菜单必须先显示出来用户先选择一次然后根据选择决定是否继续。这天然就是do-while的语义。Scanner scanner new Scanner(System.in); int choice; do { System.out.println( 系统菜单 ); System.out.println(1. 查询余额); System.out.println(2. 转账汇款); System.out.println(3. 退出系统); System.out.print(请输入你的选择); choice scanner.nextInt(); switch (choice) { case 1: System.out.println(当前余额100.00元); break; case 2: System.out.println(执行转账流程); break; case 3: System.out.println(系统退出欢迎下次使用); break; default: System.out.println(无效选项请重新输入); } } while (choice ! 3);这段代码是我早年写管理后台时特别常用的模板。注意do-while和switch配合时switch的break只跳出switch不会跳出外面的do-while循环所以选1、选2之后会回到菜单继续展示只有选3时choice ! 3为false整个循环才结束。这里两个break的差别要是没搞懂很容易误以为switch的break能“顺便”把循环也断了实际上那只是跳出了分支结构。有一个细节值得说说用scanner.nextInt()配合菜单选择时键盘输入的数字后面通常带着一个换行符。如果你在同一个Scanner上先用了nextInt()再用nextLine()那nextLine()会直接读到那个残留的换行符导致“明明让我输入却直接跳过去了”的现象。菜单循环里如果只用nextInt()倒没事但一旦你在某个分支里想读一行字符串这个坑就出来了。现在不少团队写这种交互代码会统一用nextLine()接收再解析或者直接在nextInt()之后补一个scanner.nextLine()把换行符吞掉都是为了避免这个问题。3.3 经典场景二密码验证与用户输入校验另一个十分契合do-while语义的场景是密码验证。用户至少要输入一次密码吧输入错了才需要重新输这个逻辑用do-while写起来干净利落。Scanner scanner new Scanner(System.in); String password; do { System.out.print(请输入密码); password scanner.nextLine(); if (!123456.equals(password)) { System.out.println(密码错误请重新输入); } } while (!123456.equals(password)); System.out.println(验证通过欢迎回来);同样的逻辑如果用while写你得在循环前单独写一遍输入代码或者把初始值设成空字符串强行凑进去。前者代码重复后者逻辑绕弯。do-while在这里的优点非常直观把“无论如何都得执行一次”的意图直接写在语法里而不是靠代码技巧拐弯抹角地实现。3.4 经典场景三猜数字游戏猜数字是学循环结构时最经典的练习而它用do-while写起来比while自然得多。玩家至少要猜一次猜的次数不定直到猜对为止。Random random new Random(); int target random.nextInt(100) 1; int guess; int attempts 0; Scanner scanner new Scanner(System.in); do { System.out.print(请猜一个1到100之间的数字); guess scanner.nextInt(); attempts; if (guess target) { System.out.println(猜大了); } else if (guess target) { System.out.println(猜小了); } } while (guess ! target); System.out.println(恭喜猜对共猜了 attempts 次);random.nextInt(100) 1生成1到100之间的数这是Java的Random类的基本用法。整个程序里target是预先定好的答案guess靠用户在循环里输入循环结束的唯一条件是猜中。每次猜完都给出“大了”“小了”的提示让用户有信息去调整下一轮猜测这就是一个简单但完整的反馈闭环。这种小游戏拿来练手对理解do-while的执行流程很有帮助。4. while和do-while怎么选决策依据与实战对照4.1 一张表看清两者的核心差异对比维度whiledo-while判断时机先判断条件再执行循环体先执行循环体再判断条件最小执行次数0次1次语法格式while (条件) { ... }do { ... } while (条件);分号位置结尾不需要额外分号结尾必须带分号典型场景从头开始遍历未知数据、轮询状态、等待条件满足菜单交互、用户输入校验、必须执行一次的处理死循环风险条件恒真时死循环条件恒真时同样死循环且第一次必执行这张表可以当面试前的速查备忘但实际开发里选哪个核心就看一个问题循环体有没有可能一次都不该执行如果有可能一次都不执行用while。比如你要遍历一个集合但集合可能是空的——总不能先对空集做处理吧。再比如日志采集程序里要“等待日志文件生成”文件没生成之前你当然不应该先读一遍。这种场景用while更安全。如果无论如何都得先执行一次用do-while。菜单要先弹出来密码要先让用户输一次随机数要先猜一把。这种需求你要是用while写就得在循环外面把循环体预执行一遍造成重复代码。重复代码的坏处不止是看着烦关键是以后你改循环体里的逻辑时很可能只改了一处忘了另一处那就是bug。4.2 三个实战选择题帮你看清选择逻辑我举几个实际会遇到的情况你可以先自己在心里选一下再往下看。第1题写一个程序不断接收网络请求直到收到停止指令才退出。答案是while。因为一开始根本没收到任何请求循环体一次都不该执行得等第一条请求到了才开始处理完全符合“先判断再处理”的顺序。第2题写一个定时上报程序每隔十秒上报一次系统状态上报失败就重试。答案是do-while更合适。因为上报动作必须立刻执行一次失败了才进入重试循环。用while当然也能写但你要么把上报逻辑复制一份放在循环外要么用一个boolean变量在循环里人为构造“第一个回合必须进去”的效果绕来绕去不如do-while直接。第3题写一个文件读取程序读取某个配置文件文件可能不存在。答案是while。先判断文件是否存在以及是否还有内容可读不存在就直接跳过整个循环这种情况下如果用了do-while你会在文件不存在时照样尝试读取一次打出一条无意义的错误日志还容易让日志系统判断异常。4.3 为什么do-while在实际项目里的存在感低于while和for我观察下来很多程序员一年到头写不了几个do-while这是真实的。原因是大多数业务开发面对的循环场景是遍历集合这种场景for循环更顺手还有一些场景是“直到条件满足”这种场景while更匹配。do-while适用的“必须先跑一次”场景在业务代码里的占比本来就小。但小众不等于没用。凡是涉及交互式的命令行、协议解析、数据接收缓存这类底层处理do-while的价值立刻凸显出来。举个例子解析TCP数据帧时你至少要尝试读入一帧数据才能判断这个帧是否完整读完发现不完整那就不再继续处理等待下一个数据到达。这就完美符合do-while的语义。所以我的态度是不要因为do-while用得少就轻视它你写不写得出优雅的do-while取决于你对“先做后判断”这种流程的敏感度。4.4 换个思路什么时候while反而比for更合适很多教材强调for适合固定次数的循环while适合不定次数的循环。这句话基本没问题但实际工作里有一个微妙之处即使循环次数事先能算出来有些场景用while写反而更清晰。比如遍历链表这种不能随机访问的数据结构用for的i下标去遍历就很别扭while则能直接通过“当前节点不为空”来判断是否继续。同样处理Iterator时while (it.hasNext())比写法上费劲的for循环清爽得多。说到底循环结构是给逻辑服务的工具不是给人显摆语法用的。哪个写起来让语义更直接让后续维护的人更容易理解就用哪个。5. 实战中的高频坑点与排查实录5.1 死循环怎么排查从现象到根因的三板斧死循环是while和do-while最容易引发的事故。我接手过不少线上问题现象大同小异CPU占用率飙升、请求卡住不响应、日志刷得飞快。排查思路一般按这个顺序来第1步看循环条件涉及的变量到底有没有在循环体里更新。最常见的死循环原因就是这个——循环体执行了但条件里用的变量根本没变化条件恒为true。比如下面这段int i 1; while (i 10) { System.out.println(i); }i永远是1i 10永远成立这个循环没有出口。编译不报错、运行不崩溃但它就是不结束。这种问题在review代码时一眼就能看出来但代码一多、循环体一长就容易忘记i写在哪了。第2步看条件判断是不是因为逻辑写反而变成恒真。比如i 10 i 5这种由多个子条件组合的条件由||和连接在一起时很容易出现“部分条件永远为true”的情况。特别是while (flag || process())这种写法只要flag没被重置循环就会一直跑。第3步实在找不到就直接在循环体里加日志或者用调试器逐步跟踪。最原始但有效的方法是在循环体开头打印条件表达式的值和关键变量的值看看每一轮这些值到底怎么变的。定位到某个变量的值和预期不一致那根因基本就找到了。5.2 分号陷阱do-while的语法细节不能靠感觉第一次写do-while的人十个里有三四个会把结尾的分号漏掉。这个分号不是可有可无的装饰它是do-while语法的一部分。没了它编译器会认为整个do-while结构还没结束紧接着去看下一行能不能继续拼上然后报出一个让人摸不着头脑的语法错误。我记得论坛上经常有人贴这类报错问“哪里错了”结果就是少写一个分号。相比之下while和for的结尾本来就不需要分号除非循环体是空语句所以从其他循环结构转过来写do-while时很容易按旧习惯漏写。我的建议是每次写完do-while条件那一行的末尾扫一眼有没有分号。这个习惯养成了这类低级错误基本可以杜绝。5.3 变量作用域循环内声明的变量循环外用不了新手经常踩的另一个坑是作用域问题。在循环体内部声明的变量只能在循环体内部使用出了大括号就失效。如果你在循环体里统计了一个count想等在循环结束之后打印它的值就得把这个变量声明在循环外面。int total 0; while (...) { int value ...; // 局部变量每次循环都会重新创建 total value; // total在循环外声明可以跨循环累计 } System.out.println(total); // 可以访问total但不能访问value这个规则其实不复杂但初学者容易把“在循环里用过”的变量和“在循环里声明”的变量搞混。前者没问题后者才受作用域限制。清楚区分了看别人代码时很多疑问都能解开。5.4 break和continue循环体内的控制流细节break负责提前跳出整个循环continue负责跳过本次循环的剩余部分直接进入下一轮条件判断。这在while和do-while里的行为有一个微妙差异在while里配合continue时跳过了本次循环剩下的所有代码包括更新循环变量的那部分。也就是说如果你把i放在循环体末尾并且正好被continue跳过这个循环很快就会变成死循环。// 错误示例continue跳过了i导致死循环 int i 0; while (i 10) { if (i % 2 0) { continue; // 这句把i也跳过了 } System.out.println(i); i; }这个例子里当i等于0时偶数continue直接回到条件判断i永远没执行i一直是0死循环。解决办法是把更新操作放在continue之前或者改用for循环for的更新语句在continue时仍然会执行。在do-while里continue同样会跳过循环体剩余部分但do-while的下一轮不判断条件吗判断它跳过了更新语句照样可能死循环。所以不管用哪个循环continue后一定要确保更新变量不被吞掉。5.5 常见问题速查表现象可能原因排查要点程序卡死CPU占用高循环变量没有更新检查循环体内是否有条件变量被更新continue是否跳过了更新循环一次都没执行条件判断一开始就是false确认初始状态下条件表达式的值是否符合预期结果差一个数边界条件写错应为把最小和最大值代入条件表达式中逐个验证do-while报语法错误漏写结尾分号检查} while (条件);末尾分号变量在循环外面无法使用变量作用域限制需在循环外声明变量菜单选项执行完后没有回到菜单误以为switch的break能跳出循环需要让外层do-while条件不满足才退出这张表是我自己排查问题时的常用参考它不是万能药但覆盖了大部分循环相关的低级错误。碰到棘手的就沉下心在纸上把循环的执行轨迹画出来把每次循环后变量的值写下来多画几轮规律自然就浮现了。6. 面试考点与进阶视角别只背“至少执行一次”6.1 基础题的标准答法while和do-while的区别面试官问“while和do-while的区别”很多人条件反射就答“do-while至少执行一次”然后戛然而止。这个答案只能算及格想要拿高分得把话说完整。我建议按下面这个顺序组织首先说执行时机while先判断后执行可能执行0次do-while先执行后判断至少执行1次。然后说语法差异do-while结尾需要分号。再说适用场景不确定是否执行时用while比如轮询、遍历要求至少执行一次时用do-while比如菜单展示、用户输入校验。最后如果还能补一句“能用while模拟do-while但会造成重复代码所以遇到‘先执行一次再说’的场景我会优先考虑do-while”那面试官心里基本就会给你加分了。这个层次的回答展示的不只是背过概念更是真的写过多版本代码后形成的选型判断。6.2 陷阱题判断这段代码的输出下面这段代码输出什么int x 5; do { System.out.println(x); x--; } while (x 10);答案是只输出一个5。因为do-while不管条件如何先执行一次循环体x从5变成4再判断4 10为false循环结束。很多人根据对while的印象觉得“条件不成立根本不进循环”放在do-while上就错了。这个题几乎能准确区分出到底有没有理解“至少执行一次”的含义。再看另一道很经典的int count 0; while (count 5) { count; } System.out.println(count);count最后等于5。注意循环结束的条件是count 5为false也就是count已经等于5了。如果面试官追问“循环结束后count的值是多少”很多人会脱口而出“5”但如果你习惯用i n同样的代码结束后i会是n1。这种细微差别在后续的算法题里很容易成为失分点。6.3 循环结构在设计层面的思考如何写出可维护的循环抛开应试我觉得循环结构真正值得琢磨的是“可读性”。一段业务代码别人看三秒能不能明白你在循环什么直接决定了这段代码的维护成本。我给自己定过几条规矩供参考第一循环体尽量短。太长的循环体会让读者看了一半忘记上半部分在干嘛。确实需要复杂处理时把逻辑抽成方法循环体内只留调用和判断这样while的条件和循环体目标一眼就能对上。第二条件表达式里不要写复杂的逻辑运算。while (!finished count maxTimes list.size() 0)这种一长串条件稍有不慎就看错优先级。不如拆成几步先算出一个布尔变量再拿来当条件甚至直接在循环体内判断并break让每一步的意图都清清楚楚。第三不要在循环体里悄悄改循环条件的“副作用”变量。我见过有人为了让循环提前退出在循环体深处随手把某个标志位改了看到的人一头雾水。这种隐式控制流的维护成本极高真要提前结束用break或者把判断条件写明显一点都行。第四如果循环次数已知优先考虑for。这不是说while不能用而是for把初始化、条件、更新三件事收拢在同一行里阅读成本低。但如果你发现写while能更好地表达“退出时机依赖于循环内部的计算结果”那不用犹豫while就是对的。6.4 从基础语法到工程思维循环之间藏着的那点“为什么”最后说一下我对学习循环结构的体会。语法层面while和do-while总共就那么几行半天就能全部掌握。但真正拉开差距的是你能不能迅速判断一个场景到底该用哪个循环以及遇到死循环、边界错误时能不能快速定位。这两种能力都依赖同一件事在脑子里把程序一个字一个字地“跑”一遍。我特别推荐一个笨方法练这个能力找一段带循环的代码别急着运行拿张纸把每个变量的值变化一行一行写下来一直写到循环结束。写几段之后你对条件判断时机、更新顺序、边界值的感觉会彻底不一样。这个习惯我在带新人时反复强调效果比单纯刷题好得多。还有一个实用的递进式练习思路同一道题分别用while和do-while各写一遍。比如“不断输入整数直到输入0为止计算非0数字的总和”。用while写一遍再改写成do-while写一遍你会发现两种写法在循环体外层代码布局上的差别这种体会是看十篇文章都换不来的。就我个人而言第一次真正折服于do-while的实用性是在写一个数据采集程序时每次都要先尝试抓取一帧数据再进行完整性校验起初我用while硬写把抓取逻辑复制到了循环外后来有一次改了抓取逻辑只改了循环内的一处线上直接抓到了半个帧——从那以后凡是遇到“无论如何都得先做一次”的逻辑我都会条件反射地选择do-while。这不是什么高深的理论就是踩坑踩出来的条件反射。希望你不用像我一样踩一遍完整的坑也能养成这个条件反射。
返回列表