第二单元 · 分支、循环、数组 · 第 5 / 12 节
for 与 while:重复做一件事
写 for 三段式与 while 循环,用 break 提前结束、continue 跳过本次,写浅层嵌套并解释内外层执行次数,避免死循环。
这一节继续“左边网页、右边终端”。网页给完整源码和命令,终端里真的编译、运行、看输出。$ 开头的是 shell 命令;java 代码块是完整源文件;output 是程序标准输出;本节没有 input 块,所有例子都用写死的变量。
第一单元你已经会变量、算术、比较和 Scanner;上一节会 if。这一节把“做一次”变成“重复做”:for、while、do-while,再用 break 提前结束、continue 跳过本次。本节不用数组和自定义方法;main 仍然只是入口外壳。
新开 SSH 窗口时,先 source ~/java-practice/env.sh,确认 java -version 和 javac -version 都是 17.x。本节命令默认在同一个窗口里执行;source 可能打印一行环境提示,那是 shell 的输出,不要和程序 output 混。
$ 提示符;输出和输入示例没有复制按钮。已完成 0 / 7 步
第 1 步:for 的三段式,数着次数重复
for 适合“次数已知”的重复:
for (初始化; 条件; 更新) {
循环体;
}
执行顺序不是“初始化→条件→更新→循环体”,而是:
- 初始化只做一次;
- 判断条件,
true才进入循环体; - 循环体执行完,再做更新;
- 回到第 2 步继续判断。
先预测:下面的程序会打印几次“第 N 次”?如果把 i <= 5 改成 i < 5,会打印几次?
建目录、写 ForCountDemo.java。同名文件存在时先备份,不覆盖你上次的内容:
$ source ~/java-practice/env.sh
$ java -version
$ javac -version
$ mkdir -p ~/java-practice/lesson05
$ cd ~/java-practice/lesson05
$ if [ -e ForCountDemo.java ]; then mv ForCountDemo.java "ForCountDemo.java.bak.$(date +%s)"; fi
$ nano ForCountDemo.java
把下面这个完整文件粘进 nano,按 Ctrl+O 回车保存,再按 Ctrl+X 退出:
public class ForCountDemo {
public static void main(String[] args) {
for (int i = 1; i <= 5; i++) {
System.out.println("第 " + i + " 次");
}
System.out.println("循环结束");
}
}
$ ls -l ForCountDemo.java
$ javac -encoding UTF-8 ForCountDemo.java
$ java ForCountDemo
第 1 次
第 2 次
第 3 次
第 4 次
第 5 次
循环结束
i 从 1 开始,每次循环体结束后 i++,条件 i <= 5 在 1、2、3、4、5 时都为 true,所以循环体执行 5 次;i 变成 6 时条件为 false,循环结束,接着执行循环后面的“循环结束”。
i 是 for 里声明的变量,作用范围只在 for 的圆括号和花括号里;循环结束后再写 System.out.println(i); 会编译错误。现在不用记术语,先记住“i 出了循环就不能用”。
提示 先数三处
读 for 先看三个地方:int i = 1(从几开始)、i <= 5(到几停)、i++(每次加多少)。三处一起决定次数,不要只看条件。
参考答案 先预测:改成 i < 5
只打印 4 次(1 到 4),然后打印“循环结束”。因为 i 变成 5 时,5 < 5 是 false,第 5 次进不去。把 i++ 改成 i-- 则条件 i <= 5 会一直为 true(i 越来越小),程序进入死循环;这时在终端按 Ctrl+C 停掉,不要关服务器。
第 2 步:用累加器把结果带出循环
循环经常用来“累加”:先把结果变量初始化,再在循环体里一次次更新。比如求 1 到 100 的和:
int sum = 0;
for (...) {
sum = sum + i;
}
sum = sum + i 先算右边的 sum + i,再把结果赋回左边的 sum。所以 sum 会保留上一次的值,越加越大。如果循环体里忘了写这行,sum 会一直是 0;如果把 int sum = 0; 写在循环体里面,每次循环都会把 sum 重置成 0,最后只剩最后一个数。
先预测:1 到 100 的和是多少?如果只加到 5,sum 最后是多少?
$ cd ~/java-practice/lesson05
$ if [ -e ForSumDemo.java ]; then mv ForSumDemo.java "ForSumDemo.java.bak.$(date +%s)"; fi
$ nano ForSumDemo.java
public class ForSumDemo {
public static void main(String[] args) {
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum = sum + i;
}
System.out.println("1 到 100 的和是:" + sum);
}
}
$ javac -encoding UTF-8 ForSumDemo.java
$ java ForSumDemo
1 到 100 的和是:5050
sum 在循环前是 0;第 1 次加 1 变 1,第 2 次加 2 变 3,第 3 次加 3 变 6……循环结束后 sum 就是 5050。循环体里没有打印,所以中间过程看不见,只有最后一行输出。
把 i <= 100 改成 i <= 5,和会变成 15(1+2+3+4+5),但打印文字还写着“1 到 100”,记得一起改。先在小范围核对,再推广到 100,是检查循环边界的常用方法。
第 3 步:while 把“初始化、条件、更新”分开写
while 也能重复,但三个部分不挤在一行:
初始化;
while (条件) {
循环体;
更新;
}
for 把三部分写在圆括号里,适合次数明确的循环;while 把更新写在循环体里,适合“条件满足就一直做”的循环。两者可以互相改写,但 while 更容易漏掉更新。
先预测:下面的程序会打印哪些数字?如果删掉 i--; 这一行,会发生什么?
$ cd ~/java-practice/lesson05
$ if [ -e WhileCountDemo.java ]; then mv WhileCountDemo.java "WhileCountDemo.java.bak.$(date +%s)"; fi
$ nano WhileCountDemo.java
public class WhileCountDemo {
public static void main(String[] args) {
int i = 5;
while (i >= 1) {
System.out.println(i);
i--;
}
System.out.println("发射!");
}
}
$ javac -encoding UTF-8 WhileCountDemo.java
$ java WhileCountDemo
5
4
3
2
1
发射!
i 从 5 开始,条件 i >= 1 为 true,打印 5,然后 i-- 变成 4;再判断、再打印……直到 i 变成 0,条件为 false,循环结束,打印“发射!”。
如果删掉 i--;,i 永远是 5,5 >= 1 永远为 true,程序会不停打印 5,这就是死循环。在终端按 Ctrl+C 可以停掉当前程序;不要用关闭服务器或杀其他进程的方式解决。
| 对比 | for | while |
|---|---|---|
| 初始化 | 写在圆括号里,只执行一次 | 写在循环前 |
| 条件 | 圆括号中间 | 圆括号里 |
| 更新 | 圆括号最后 | 循环体里自己写 |
| 适合 | 次数明确 | 条件满足就继续 |
注意 死循环的第一检查点
程序不停输出同一行、终端没有回到提示符,先看循环变量有没有更新。while 的更新写在循环体里,最容易漏;漏了就先 Ctrl+C,再补更新,不要反复运行。
第 4 步:do-while 先做一次,再判断
do-while 是“先执行循环体,再判断条件”:
do {
循环体;
} while (条件);
注意 while (条件) 后面有分号。即使条件一开始就是 false,循环体也会执行一次;这是它和 while 的唯一区别。本阶段只要求认识它,不要求用它写复杂逻辑。
先预测:i = 10,条件 i < 3 一开始就是 false,循环体会执行几次?
$ cd ~/java-practice/lesson05
$ if [ -e DoWhileDemo.java ]; then mv DoWhileDemo.java "DoWhileDemo.java.bak.$(date +%s)"; fi
$ nano DoWhileDemo.java
public class DoWhileDemo {
public static void main(String[] args) {
int i = 10;
do {
System.out.println("执行一次,i = " + i);
i++;
} while (i < 3);
System.out.println("结束,i = " + i);
}
}
$ javac -encoding UTF-8 DoWhileDemo.java
$ java DoWhileDemo
执行一次,i = 10
结束,i = 11
循环体先执行,打印 i = 10,然后 i++ 变成 11;这时才判断 11 < 3,结果为 false,循环结束。虽然条件一开始就不成立,循环体还是执行了一次。把 do-while 改成 while,同样的 i = 10 一次都不会执行。
第 5 步:break 提前结束整个循环
break 的作用是立刻跳出当前这一层循环,不再判断条件,也不执行循环体剩下的部分。常见用途:找到一个满足条件的数就停。
先预测:1 到 50 中,第一个 7 的倍数是多少?循环会执行多少次?
$ cd ~/java-practice/lesson05
$ if [ -e BreakDemo.java ]; then mv BreakDemo.java "BreakDemo.java.bak.$(date +%s)"; fi
$ nano BreakDemo.java
public class BreakDemo {
public static void main(String[] args) {
int target = -1;
for (int i = 1; i <= 50; i++) {
if (i % 7 == 0) {
target = i;
break;
}
}
System.out.println("1 到 50 中第一个 7 的倍数是:" + target);
}
}
$ javac -encoding UTF-8 BreakDemo.java
$ java BreakDemo
1 到 50 中第一个 7 的倍数是:7
i 从 1 开始,前 6 次 i % 7 == 0 都是 false;i 到 7 时条件为 true,把 target 设为 7,break 立刻跳出 for,后面的 8 到 50 不再检查。target 在循环前初始化为 -1,是为了让变量在循环结束后仍然有值可打印;如果循环一次都没进,-1 就是一个“没找到”的标记。
break 只跳出离它最近的一层循环;如果是在 switch 里,它跳出的是 switch。本节只讨论循环里的 break。如果后面写嵌套循环,内层 break 不会把外层也停掉;这一点在第 7 步之后要记住。
参考答案 先预测:从 8 开始找
如果循环写成 for (int i = 8; i <= 50; i++),第一个满足 i % 7 == 0 的数是 14,所以输出 14。循环从 8 到 14 共执行 7 次,第 7 次才碰到 14。
第 6 步:continue 跳过本次,继续下一次
continue 不结束整个循环,它只跳过当前这一次循环体里剩下的语句,直接进入下一次的更新和条件判断。
先预测:1 到 10 中,把所有“能被 3 整除”的数跳过,剩下的数相加是多少?
$ cd ~/java-practice/lesson05
$ if [ -e ContinueDemo.java ]; then mv ContinueDemo.java "ContinueDemo.java.bak.$(date +%s)"; fi
$ nano ContinueDemo.java
public class ContinueDemo {
public static void main(String[] args) {
int sum = 0;
for (int i = 1; i <= 10; i++) {
if (i % 3 == 0) {
continue;
}
sum = sum + i;
}
System.out.println("1 到 10 中不能被 3 整除的数的和:" + sum);
}
}
$ javac -encoding UTF-8 ContinueDemo.java
$ java ContinueDemo
1 到 10 中不能被 3 整除的数的和:37
i 为 3、6、9 时,i % 3 == 0 成立,continue 跳过 sum = sum + i;,直接进入下一次 i++;其他数正常累加。1+2+4+5+7+8+10 = 37。
在 for 里,continue 之后仍会执行圆括号里的 i++,所以不会死循环。在 while 里要特别小心:如果更新写在 continue 后面,continue 会跳过更新,循环变量不变,可能变成死循环。初学阶段先把更新写在循环体最前面,或改用 for。
| 关键字 | 作用范围 | 循环还继续吗 |
|---|---|---|
break | 跳出最近一层循环 | 不继续,整个循环结束 |
continue | 跳过本次剩余语句 | 继续下一次 |
第 7 步:浅层嵌套,内外层各跑几次
循环里再写一个循环叫嵌套。外层每走一次,内层都要从头到尾走一遍。下面用两层循环打印 3 行、每行 4 个星号:
$ cd ~/java-practice/lesson05
$ if [ -e NestedDemo.java ]; then mv NestedDemo.java "NestedDemo.java.bak.$(date +%s)"; fi
$ nano NestedDemo.java
public class NestedDemo {
public static void main(String[] args) {
String line = "";
for (int row = 1; row <= 3; row++) {
line = "";
for (int col = 1; col <= 4; col++) {
line = line + "*";
}
System.out.println(line);
}
System.out.println("共 3 行,每行 4 个星");
}
}
$ javac -encoding UTF-8 NestedDemo.java
$ java NestedDemo
****
****
****
共 3 行,每行 4 个星
外层 row 从 1 到 3,共 3 次;每次外层循环体里,先把 line 清空,再让内层 col 从 1 到 4 走 4 次,每次往 line 后面接一个 *。内层结束后 line 是 "****",打印出来并换行。所以总共打印 3 行,每行 4 个星,内层循环体一共执行 3 × 4 = 12 次。
先预测:如果把外层改成 row <= 2、内层改成 col <= 5,会打印几行、每行几个星?内层循环体总共执行多少次?
参考答案 先预测:2 行 5 列
会打印 2 行,每行 5 个星;内层循环体执行 2 × 5 = 10 次。外层每走一次,内层都从 col = 1 重新开始,所以不是 5 次,而是 10 次。
注意 嵌套里的 break 只跳一层
如果在内层循环里写 break,只会结束内层,外层还会继续下一行。想让两层都停,需要在外层也判断并 break,或者用一个标记变量;本阶段先不写复杂跳出,看到嵌套 break 时先确认它停的是哪一层。
常见错误
- 现象: 循环多打印一次或少打印一次,比如 1 到 5 只打印了 4 次。原因: 边界写成了
i < 5而不是i <= 5,或初始值从 0 开始。先检查: 把i的初始值、条件、更新三处列出来,手工走前两次和最后一次。 - 现象:
for或while后面多了一个分号,循环体只执行一次或根本没按预期重复。原因:for (...);或while (...);让循环体变成空语句,后面的花括号不再属于循环。先检查: 看右括号后有没有多余的;。 - 现象: 程序不停输出同一行,终端回不到提示符。原因: 循环变量没有更新,或更新方向写反(该加却减),条件永远为
true。先检查: 按Ctrl+C停掉,再找i++/i--是否在循环体里、是否被continue跳过。 - 现象: 嵌套循环里
break后,外层还在继续跑。原因:break只跳出最近的一层,不会自动跳出外层。先检查: 确认break写在哪一层花括号里;需要两层都停时,用标记变量或在外层加条件。 - 现象:
while里用了continue后程序卡住。原因:continue跳过了后面的i++,循环变量不变,条件永远成立。先检查: 把更新移到continue之前,或改用for。 - 现象:
javac报cannot find symbol,指向循环结束后的i。原因:for (int i = ...)里的i只在循环内可见,出了循环就没了。先检查: 如果循环后还要用i,把int i;声明在循环外面,循环里只写i = 1。
练习
先自己写、先预测,再展开答案。练习答案都在 JDK 17 上实际编译运行过,output 是真实标准输出。
练习 1:求 1 到 20 的偶数和
用 for 和 if(或 continue)求 1 到 20 中所有偶数的和,输出一行结果。先预测结果,再写代码。
参考答案 练习 1:偶数和
完整文件存成 EvenSumDemo.java:
public class EvenSumDemo {
public static void main(String[] args) {
int sum = 0;
for (int i = 1; i <= 20; i++) {
if (i % 2 == 0) {
sum = sum + i;
}
}
System.out.println("1 到 20 中偶数的和:" + sum);
}
}
编译运行:
$ javac -encoding UTF-8 EvenSumDemo.java
$ java EvenSumDemo
1 到 20 中偶数的和:110
2+4+6+…+20 = 110。i % 2 == 0 为 true 时才累加;也可以把条件反过来,用 continue 跳过奇数,结果一样。
练习 2:打印 4 行 3 列的井号
用两层 for 打印 4 行,每行 3 个 #,最后再打印一行“共 4 行,每行 3 个 #”。仍然用字符串拼接,不要用还没讲的 System.out.print。
参考答案 练习 2:4 行 3 列
完整文件存成 HashGridDemo.java:
public class HashGridDemo {
public static void main(String[] args) {
String line = "";
for (int row = 1; row <= 4; row++) {
line = "";
for (int col = 1; col <= 3; col++) {
line = line + "#";
}
System.out.println(line);
}
System.out.println("共 4 行,每行 3 个 #");
}
}
编译运行:
$ javac -encoding UTF-8 HashGridDemo.java
$ java HashGridDemo
###
###
###
###
共 4 行,每行 3 个 #
外层 4 次、内层每次 3 次,内层循环体共执行 12 次;每行开始前把 line 清空,所以不会把上一行的 # 带到下一行。
练习 3:先预测循环次数
下面的循环只是片段,不用单独存成文件;先口算会打印几行,再展开答案。
int count = 0;
for (int i = 1; i <= 3; i++) {
for (int j = 1; j <= 2; j++) {
count++;
System.out.println("i=" + i + ", j=" + j);
}
}
System.out.println("count=" + count);
参考答案 练习 3:3 × 2
打印 6 行 i=..., j=...,最后一行 count=6。外层 i 有 3 个值,每个 i 下内层 j 都从 1 到 2 走 2 次,所以内层循环体执行 3 × 2 = 6 次,count 从 0 加到 6。
做到这里
做到这里今天可以停。你已经会:
- 用
for的三段式控制次数,并知道初始化、条件、更新的执行顺序; - 用
while在条件成立时重复,知道更新漏写会死循环; - 认识
do-while的“先做一次再判断”; - 用
break提前结束、用continue跳过本次; - 写两层嵌套,并算清内外层各执行多少次。
下一节才学数组:把 5 个成绩放进一排带编号的格子,用下标和 length 遍历。本节还没有数组、没有自定义方法;练习里不要提前用数组或写 static 方法。
来源 来源与改编说明(非官方)
主题参考:黑马程序员 Java 入门到起飞(BV17F411T7Ao,2022-04-27)P045–P050 的 for、while、do-while、无限循环与跳转控制主题。本站为独立原创讲解,非官方站点/教材,未获授权,不逐字复刻视频或讲义;示例、错误清单与练习为原创补充。详见 source/SOURCE.md。
说明:本站是个人学习整理,不是官方教材;不包含原课程的书本、视频或受版权保护的整本内容。原课程与商标归其权利人。