第004篇 循环 for/while/do-while:遍历与终止的工程课

简介: Android面试中,循环看似简单,实则暗藏细节:终止条件、步长控制、对象分配、GC压力、快速失败机制、浮点误差、多层跳出等均是高频追问点。掌握for/while/do-while本质差异、增强for底层原理及工程避坑(如遍历中禁用list.remove),方能稳过此关。

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:分支逻辑规范写法

下一篇预告:数组与多维数组:批量数据的地基

有任何问题欢迎在评论区留言交流。

相关文章
|
1天前
|
人工智能 自然语言处理 运维
找国内智能客服系统厂商推荐?这家企业的AI客服方案值得优先关注
2025年智能客服市场达71.9亿元,正从“能答”迈向“能干”。阿里云瓴羊Quick Service以AI Agent为核心,构建“感知—理解—决策—执行”闭环,支持查物流、改地址、退款等自主操作,任务完成率超80%,已服务5万家企业,覆盖多行业全渠道场景。
|
1天前
|
存储 安全 编译器
第070篇 扩展函数原理:它到底是不是给类加方法
Kotlin扩展函数本质是**静态方法**,编译后以接收者为首个参数,置于文件类中;调用依**静态类型决议**,不参与多态,**不访问private成员**,且**成员方法永远优先于同名扩展**——它是语法糖,而非真正的方法增强。
21 1
|
2天前
|
Java 编译器 Android开发
第014篇 重载与重写:编译期与运行期的分野
重载与重写是Java多态的两大基石:重载发生于同一类,编译期按参数列表静态绑定;重写发生于父子类,运行期按实际类型动态分派。二者易混淆,但本质分属编译期与运行期,规则、边界与典型坑(如忘加`@Override`、static隐藏、泛型擦除冲突)须清晰辨析。
21 0
|
1天前
|
安全 编译器 API
第116篇 Kotlin 代码评审清单:团队规范与静态检查
本节聚焦Kotlin团队代码质量保障机制,提出“三层金字塔”模型:①工具层(ktlint/detekt/编译器)自动化查格式与确定性缺陷;②约定层(PR模板/基线/baseline)半强制落地规范;③设计层(架构/抽象/命名)依赖人工判断。核心是“机器管规则,人管设计”,避免告警泛滥与评审失焦。
19 0
|
1天前
|
安全 Java Android开发
第104篇 Kotlin 惯用法精选:一行替代十行 Java
本文精讲Kotlin五大高阶惯用法:空安全流、集合流水线、作用域函数选择、解构与数据类、契约式前置条件。聚焦面试真题——“如何写得更Kotlin”,强调每种惯用法的**适用场景**与**禁用时机**,避免为链而链。附机制解析、工程反例、调试避坑及团队落地规范。
17 0
|
1天前
|
缓存 Java API
第050篇 IO 流体系:字节流与字符流的分层设计
Java IO流是面试分水岭:字节流处理二进制,字符流处理文本(内部仍转字节);装饰器模式实现缓冲、数据/对象序列化等能力。核心考点:缓冲区优化原理、字符编码陷阱(必须显式指定UTF-8)、try-with-resources资源管理、批量读写避坑。真实项目中,用错流类型、忽略编码、未关闭流是高频故障源。
23 0
|
1天前
|
监控 Java Android开发
第045篇 JVM 内存区域:堆、栈、方法区与程序计数器
JVM内存区域是面试分水岭:非死记硬背,而要理解线程私有(栈、PC计数器、本地方法栈)与共享(堆、元空间)的本质差异,能据OOM类型精准定位根因——栈溢出看调用链、堆OOM析对象引用、元空间OOM查类加载、直接内存OOM盯NIO缓冲。
19 0
|
1天前
|
SQL Java 编译器
第040篇 volatile:可见性、有序性与禁止重排
`volatile` 是Java轻量级同步机制,核心解决**可见性**与**有序性**(靠内存屏障实现),但**不保证原子性**。典型适用:状态标志、引用发布、DCL单例;禁用场景:`count++`、多变量一致性、检查后动作——这些须用原子类或锁。
20 0
|
1天前
|
安全 Java API
第086篇 泛型型变 in 与 out:声明处型变
Kotlin型变核心是“位置约束”:`out T`仅用于返回位(协变,只读),`in T`仅用于参数位(逆变,只写);越界即编译报错。`in out T`等价于不变。声明处型变比Java使用处更安全——直接禁用非法操作(如`List&lt;out T&gt;`无`add`)。协程/Flow等API依赖此机制实现类型安全组合。
17 0
|
2天前
|
消息中间件 Java 调度
第038篇 Thread 与 Runnable:线程生命周期全解
Thread与Runnable本质是“任务”与“执行单元”的分离:Runnable仅定义要做的事(无返回、不抛检异常),Thread负责调度执行(含状态、优先级、生命周期控制)。`start()`才真正启新线程,`run()`只是普通方法调用。常见坑包括误调`run`导致伪并发、异常静默终止、持有Activity引发内存泄漏。工程中应优先使用线程池而非裸Thread。
20 0

热门文章

最新文章