Android 面试里,循环 for/while/do-while 看着简单,追问起来全是细节。它常被包装成一道开放题:"你在项目里是怎么用的?"这道题答不好,往往不是不会写循环,而是很少把"循环里到底发生了什么"讲清楚——从终止条件、迭代步长到循环内的对象分配,每一层都能延伸出追问。把循环从语法讲到代价,基本就稳了。
先把结论立住:循环的本质是"重复直到不满足"
一句话:循环就是"在条件满足时反复执行同一段逻辑",差别只在"先判断还是先执行"以及"迭代变量怎么推进"。for 把初始化、条件、步长收在一行,适合已知次数的遍历;while 先判断后执行,适合不确定次数、靠状态退出的场景;do-while 先执行一次再判断,保证主体至少跑一遍。
这里要先立住一个性能前提:循环内创建对象会放大 GC 压力,热点路径上应当把不变的对象或计算结果提升到循环外,只算一次。另一点是增强 for(for-each)底层依赖 Iterator 或数组索引,它把遍历细节藏起来了,也顺带规避了下标越界,但代价是不能在遍历中安全地结构性修改集合。
机制拆解:三种循环的终止与变体
for 循环的三个部分(初始化、条件、步长)在每次迭代前后各跑一遍,条件为假时退出。while 在每轮开头求值,条件一开始就为假则一次都不执行。do-while 把条件放到轮尾,主体至少执行一次——这正是它和 while 最本质的区别,也决定了它只适合"先做一次再看要不要继续"的场景。
增强 for 循环遍历集合时,实际调用的是 Iterator 的 hasNext/next;遍历数组时直接用下标。关键点在于:Iterator 在结构性修改(add/remove)时会抛出 ConcurrentModificationException,这是"快速失败"机制在保护你,而不是偶然报错。标签语法 break outer 可以一次跳出多层循环,但滥用会严重伤害可读性,能拆成方法返回的尽量拆。
代码里见真章
看一段能直接跑的代码,把上面的机制落到具体写法上:
// 循环:for / while / do-while 与终止、遍历、跳出
for (int i = 0; i < 10; i++) {
if (i == 3) continue; // 跳过本次,进入下一轮
if (i == 8) break; // 直接结束整个循环
}
System.out.println(i); // 循环外仍可访问,值为 8
int[] xs = {
1, 2, 3};
for (int v : xs) System.out.print(v + " "); // 增强 for:只读遍历,不暴露下标
// do-while:先执行一次,再判断条件
int n = 0;
do {
n++; } while (n < 0); // 条件一开始就为假,但仍执行了一次
// 标签跳出多层循环(慎用:可读性代价高)
outer:
for (int a = 0; a < 3; a++) {
for (int b = 0; b < 3; b++) {
if (a == 1 && b == 1) break outer; // 一次跳到 outer 之后
}
}
// 增强 for 中结构性修改集合会触发快速失败
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
for (String s : list) {
// list.remove(s); // 运行期抛 ConcurrentModificationException
}
// 正确做法:用迭代器或 removeIf
list.removeIf(s -> s.equals("b"));
这段代码值得盯三处:第一处,for 的 break/continue 控制的是"轮次",循环变量在循环外依然可见;第二处,do-while 即便条件一开始就为假也至少执行一次,这是它和 while 的分水岭;第三处,增强 for 里直接 remove 会触发快速失败,改用迭代器或 removeIf 才是正解。面试讲到这一层,基本就稳了。
最常见的几个坑
最常见的坑是在增强 for 里调用 list.remove 触发 ConcurrentModificationException,正确做法是用迭代器的 remove,或者 Java 8 之后的 removeIf。很多人栽在这,是因为把"遍历"和"修改"当成同一件事,而 Iterator 的设计本就是两者分离。
其次是 while 循环条件里调用有副作用的读取方法,读到末尾返回 null 才退出,容易漏掉资源关闭。比如一边读流一边判断,循环结束没进到 finally 去关流,连接和句柄就悄悄泄漏了。把资源的关闭放进 try-with-resources,比依赖循环退出后手动关更稳。
还有一个更隐蔽的坑:循环内的字符串拼接用 +=,每次都创建新对象并拷贝旧内容,在热路径上会迅速放大 GC 压力。应该改用 StringBuilder,或者把不变的部分提到循环外只拼一次。这不是风格问题,是能用 systrace 量出来的真实卡顿来源。
再一个是用浮点数做循环步长或终止判断,浮点累积误差会让循环次数和预期不符,甚至无限循环。计数类循环一律用整数索引,需要浮点步进时显式控制最大迭代次数兜底。
面试中的经典追问
"请简单介绍一下 for/while/do-while,它们在 Android 开发中起什么作用?"——先一句话说清三者区别:for 已知次数、while 先判后执行、do-while 至少一次;再拿项目场景证明你真用过,比如用 while 轮询下载进度、用 for 批处理列表。do-while 至少执行一次,标签语法 break outer 可一次跳出多层循环,但滥用会严重伤害可读性。按"结论、依据、边界"三步走最稳。
"循环的底层原理是什么?能不能详细说一下?"——别停在 API 表层:按动机、机制、代价讲。动机是"重复执行",机制是条件求值加迭代推进,代价是循环内分配带来的 GC、下标越界与快速失败。换个角度:如果让你给循环做一次性能画像,你会量哪些指标?迭代次数、单次耗时分布、分配对象数,这三项是起点。
"在使用循环时遇到过什么问题?"——讲问题先交代现场,再讲排查路径与根因,收尾给预防措施;全程围绕一次真实踩坑。比如曾在列表遍历里顺手 remove 导致偶发崩溃,稳定复现后用二分法定位到某个分支的修改,修复方式是改成 removeIf,并补了一条"遍历中禁止结构性修改"的团队约定。
"循环和相关的替代方案相比,有什么优劣?"——先列可选方案:增强 for、Iterator、Stream、递归;再按性能、可读性、可维护性逐项对比,最后给场景结论。选型时把"while 条件里调用有副作用的读取方法容易漏关资源"这类代价摆到台面上,再决定是否引入。
工程落地:真实项目里怎么用
循环在项目里的高频落点不少:一是批量持久化或网络请求时,用 for 分批,每批控制数量避免一次性打满内存;二是用 while 轮询任务状态或下载进度,退出条件必须明确且能兜底超时,防止死循环卡住主线程;三是列表处理优先增强 for 或 Stream,既省下标管理又规避越界;四是热路径上的循环把对象创建和不变计算提到循环外,必要时用 StringBuilder 累积。
一个稳妥的工程约定:任何 while 循环都必须有"超时或最大次数"兜底;遍历中需要删除元素一律走迭代器或 removeIf;循环体内的不变分配提到外层;超过三层或逻辑复杂的循环体抽成独立方法,让主流程保持可读。把这几条固化成 lint 规则,比靠 code review 人盯人稳得多。
还有一处值得量化的细节:for 循环的初始化变量作用域只在这一层循环内,而把计数器声明在循环外会在方法栈上多留一个存活槽位。绝大多数时候无足轻重,但在被 JIT 内联的热点方法里,减少栈上存活变量能帮助逃逸分析更激进地优化对象分配。由此得到一个提醒:循环写法在"能跑"之上,还藏着可被 JVM 优化的空间,热点路径值得用 Profiler 实际看过再下结论,而不是凭感觉选写法。
给正在准备面试的你
理解循环之后,需要在真实项目里练一遍:写一段增强 for 遍历并故意在循环里 remove,看它抛什么、为什么抛;再写一段 while 轮询加超时兜底,确认它不会卡死。这两步做下来,比再读三遍语法书都管用。
如果只记两句话,就记这两句:第一,增强 for 中结构性修改集合会触发快速失败,删除走迭代器或 removeIf;第二,任何 while 都必须有超时或最大次数兜底,循环内的不变分配提到外层。把这两句讲顺,循环这一关就过了。
如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。你的支持就是这个系列持续更新的动力。
「Android 软件开发面试·从入门到精通」连载系列
上一篇:流程控制-if-else-与-switch:分支逻辑规范写法
下一篇预告:数组与多维数组:批量数据的地基
有任何问题欢迎在评论区留言交流。