上一篇我们让程序学会了“看情况办事”:一个 if 判断完,最多执行一遍就结束。但真实程序里,重复才是常态——给全班 50 个同学判分,总不能写 50 遍 if;闹钟要每秒检查一次时间,更不能只检查一遍。

本篇只解决一个问题:一段代码如何按条件反复执行,并且在该停的时候恰好停下来。我们把这种能力叫 循环(loop)。Java 的循环有几种写法,本篇把最常用的 whilefor 讲透,do-while 和 for-each 留到文末各用一句话预告。

while:先问“还要继续吗”,再干一轮活

while 的骨架和 if 长得几乎一样:

1while (条件) {
2    循环体语句
3}

括号里的条件同样必须是一个 boolean——上一篇学的“比较运算符当场算出 true 或 false”在这里原样适用。区别全在行为上:if 算完一次就散场,while 算完若是 true,就把整个循环体从头到尾执行一遍,然后回到条件处再问一次。如此反复,直到某一次条件算出 false,才跳出循环,继续执行循环体之后的代码。

官方教程对 while 的定义正是如此:只要条件为 true,它就持续执行语句块,不断重新测试表达式,直到表达式为 false 1

正在绘制图表

这个“回到条件处再问一次”的回路就是循环的心脏。有一个推论值得先记住:如果条件一开始就是 false,循环体 一次都不会执行——while 是先问再做,不是先做再问。

for:把“数着次数干活”写成固定格式

看一个具体需求:打印 1、2、3 三个数。用 while 写是这样:

1int i = 1;                  // ① 准备:从 1 开始
2while (i <= 3) {            // ② 检查:还没到 3 就继续
3    System.out.println(i);  // ③ 干活:打印
4    i++;                    // ④ 更新:i 加 1
5}

四件事各就各位,但写多了容易漏——尤其是第④步,忘了它就成了死循环(后面会讲)。Java 把这种“数着次数干活”的常见套路压缩成了一行 头部

1for (int i = 1; i <= 3; i++) {
2    System.out.println(i);
3}

for 括号里用分号隔开三部分,官方教程逐一说明了它们的职责 2

  • 初始化int i = 1):进入循环前只执行一次,负责准备计数器;
  • 终止条件i <= 3):每次循环开始前检查,算出 false 就结束整个循环;
  • 更新i++):每一轮循环体执行完之后 执行,负责让计数器前进。

这里的 i++ 是“把 i 的值加 1 再放回 i”的简写,等价于 i = i + 1。新手最容易搞错执行顺序:更新不是和条件一起在顶部检查的,它排在循环体之后、下一轮检查之前。完整走一遍上面那段代码:i = 1 执行一次 → 检查 1 <= 3 为 true → 打印 1 → i 变成 2 → 检查 2 <= 3 为 true → 打印 2 → i 变成 3 → 打印 3 → i 变成 4 → 检查 4 <= 3 为 false → 结束。输出恰好是 123 三行。

还有个细节:i 声明在 for 的头部,它的作用范围只到这个 for 循环结束为止,循环外面用不到它。官方教程明确建议:控制循环的变量只在循环内需要时,就声明在初始化表达式里,能少惹很多麻烦 2。拿 ijk 当计数器也是 Java 世界的长期惯例。

计数器从 0 还是 1 起步

两种写法都合法,差别只在你想表达什么。

想数“1 到 n”,就写 i = 1; i <= n。前面打印 1、2、3 用的就是它。

想“恰好做 n 轮”,Java 社区几乎总写 i = 0; i < n。原因会在学数组时彻底揭晓:数组的下标从 0 数起,第 1 个元素是 array[0],所以遍历 n 个元素的惯用式是 i < n

差一错误(off-by-one error)就埋伏在这两种写法之间:i <= 3 会跑 3 轮(i 取 1、2、3),而 i < 3 只跑 2 轮(i 取 1、2)。写循环前先问自己一句:“我想数到某个数,还是想走满某个次数?”这决定了用 <= 还是 <

用 while 还是 for:看“要不要事先数好”

两条路都能写出同一个循环,但有一个朴素的判断标准:循环次数在进入之前就知道,用 for;次数取决于运行过程中发生什么,用 while

“从 1 加到 100”是一眼能数清的任务,天然适合 for。而“从 1 开始不断翻倍,直到超过 100”该执行几轮,取决于 1 翻倍多少次才会越过 100——没人想提前数,用 while 更贴切:

1int n = 1;
2while (n <= 100) {
3    n = n * 2;
4}
5System.out.println(n);   // 128

每轮循环体把 n 变成原来的两倍:1、2、4、8、16、32、64、128。到 n = 128 时条件 128 <= 100 为 false,循环停下,打印 128。循环体里改的不是计数器,而是决定条件真假的那个状态——这正是 while 的主场。硬用 for 也能写,但要把翻倍动作塞进头部第三格,远不如 while 直白。

一句话总结:看到“重复 N 次”用 for,看到“重复直到某个状态发生”用 while

三个新手坑:死循环、差一、多余的分号

死循环。 循环体里没有任何东西能让条件变假时,程序就永远跳不出来:

1int i = 1;
2while (i <= 3) {
3    System.out.println(i);
4    // 忘了写 i++:i 永远是 1,条件恒为 true
5}

这不是报错,而是程序“卡住”——控制台刷屏或毫无响应,比编译错误更难察觉,因为编译器认为代码完全合法。for 的三个部分其实都可以省略,写 for (;;) 也会造出一个死循环 2,有时是有意为之(配合后面会学的 break 跳出),但新手阶段先把它当事故。

差一错误。 就是上一节说的“多跑一轮、少跑一轮”。它不报错,只给出错的结果,是循环 bug 里最常见的一种。

条件后误加分号。 这是最阴险的一个:分号本身是一条“什么都不做的空语句”,而 Java 里循环体可以只有一条语句——

1int i = 1;
2while (i <= 3); {        // 分号让空语句成了循环体!
3    System.out.println(i);
4}

while 现在只管辖那个分号:反复检查 i <= 3,而 i 永远不变,程序死循环在分号处,后面的花括号块成了循环外的普通代码块,一行也轮不到。for 版本同样中招:for (int i = 0; i < 3; i++); 会把空循环体跑完 3 轮,再把后面花括号里的代码当普通块执行一次——程序没死,但行为完全不是你想的那样。写完 while (…)for (…) 后看一眼:这一行末尾不该有分号

收尾例子:从 1 加到 100

把前面学的变量、条件、循环串成一个小程序——著名的 1 加到 100:

1int sum = 0;                       // 累加器,先装 0
2for (int i = 1; i <= 100; i++) {   // i 从 1 数到 100
3    sum += i;                      // 等价于 sum = sum + i
4}
5System.out.println(sum);           // 5050

有两点值得停下来看。sum += i 是“把 sumi 相加,结果放回 sum”的简写,和 i++ 是同一族运算符。sum 声明在循环 外面,因为循环结束后还要用它;i 声明在 for 头部,循环一结束就作废——两类变量各有各的寿命。

观察这个循环,会发现它集齐了所有正常循环的三要素:开始前的准备(sum = 0i = 1)、继续的条件(i <= 100)、每轮结束让条件朝“假”靠近一步的变化(i++)。缺了任何一件,程序要么不停、要么结果不对——对照前文的三个坑,它们本质上都是这三要素缺位。

循环家族还有两个成员本篇没展开:do-while 保证循环体至少执行一次(它把条件检查挪到了底部),for-each 则是遍历数组和集合时更简洁的写法——两者等到后面接触数组和集合时再正式见面。