for 与增强 for
本教程共 100 篇 · 第 22 篇 · 更新于 2026-08-05 · 约 17 分钟阅读
22. for 与增强 for
本节目标:掌握 for 计数循环的三段式写法,学会用增强 for(for-each)干净地遍历数组和集合,知道它和普通 for 各自适合什么场景。
for 循环是 Java 里用得最多的循环。它把「从哪开始、到哪结束、每轮怎么变」三件事写在一行里,特别适合「知道要跑多少圈」的场景。
for 想解决什么问题
上一章的 while 已经能跑循环了,为什么还要 for?看两段等价代码就明白:
// while 版本:三个要素散落三处
int i = 1; // 初始化在循环前
while (i <= 5) { // 条件在这
System.out.println(i);
i++; // 步进埋在循环体最后一行
}
```bash
```java
// for 版本:三个要素挤在一行,一眼看全
for (int i = 1; i <= 5; i++) {
System.out.println(i);
}
差别不只是短。while 版本的 i++ 藏在循环体里,循环体一长就容易被漏看、被误删,死循环就这么来的。for 把它提到最显眼的位置,写的人不容易忘,读的人不用往下翻。
for 的基本写法
for 后面括号里有三部分,用分号隔开:
public class ForDemo {
public static void main(String[] args) {
for (int i = 1; i <= 5; i++) { // 初始化; 条件; 步进
System.out.println("第 " + i + " 圈");
}
}
}
```bash
- `int i = 1`:初始化,只在进入循环前执行一次。
- `i <= 5`:每轮开始前判断,成立才进循环体。
- `i++`:每轮循环体跑完后再执行,让计数器前进。
三者谁都不能省,但都可以留空(比如把初始化放外面)。只是习惯上三段都写上,代码最清楚。
### 三段式的执行顺序
顺序不是从左到右一遍走完,而是有个固定循环。拿上面的例子完整走一遍:
1. 执行 `int i = 1`。**这一步只做一次**,以后再不执行。
2. 判断 `i <= 5`。1 <= 5 成立 → 进循环体。
3. 执行循环体,打印「第 1 圈」。
4. 执行 `i++`,i 变成 2。
5. 回到第 2 步判断,成立就继续,不成立就跳出。
关键在第 4 步的位置:**步进是在循环体跑完之后才执行的**,不是进循环体之前。所以循环体里看到的 i,永远是「本轮的值」,不是「已经加过 1 的值」。
循环正常结束时 i 是 6,因为要 `6 <= 5` 判断失败才退得出来。
## 循环变量的作用域
`for (int i = ...)` 里声明的 i,只在这个 for 内部活着。循环一结束,它就没了:
```java
for (int i = 0; i < 3; i++) {
System.out.println(i);
}
System.out.println(i); // 编译错误:i 在这里已经不存在
ForDemo.java:7: error: cannot find symbol
System.out.println(i);
^
symbol: variable i
location: class ForDemo
```java
这是好事,不是限制。变量用完即焚,下一个循环还能接着叫 i,互不打架。要是循环结束后确实还要用这个值,就把变量声明到 for 外面:
```java
int i; // 声明在外面
for (i = 0; i < 3; i++) { // 初始化段只赋值,不声明
// ……
}
System.out.println("退出时 i = " + i); // 3
用 for 求和
public class SumDemo {
public static void main(String[] args) {
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i; // 累加 1 到 100
}
System.out.println("1到100的和:" + sum);
}
}
```bash
用来收集结果的变量要放在循环外初始化。别写进 for 的初始化里,否则每轮都重置成 0,结果永远是最后一次加的值。
## 三段其实都能省
三段都是可选的,分号必须留着。几种常见变体:
```java
int i = 0;
for (; i < 5; i++) { } // 省初始化:变量在外面已经准备好
for (int j = 5; j > 0; ) { // 省步进:改到循环体里做
System.out.println(j);
j--;
}
for (;;) { // 三段全省:等价于 while (true)
break; // 必须自己想办法跳出
}
初始化段还能塞多个变量,用逗号隔开。这在「两头往中间靠」的场景里很好用:
int[] arr = {1, 2, 3, 4, 5};
for (int left = 0, right = arr.length - 1; left < right; left++, right--) {
int tmp = arr[left]; // 交换首尾元素,把数组反转
arr[left] = arr[right];
arr[right] = tmp;
}
```bash
`int left = 0, right = arr.length - 1` 一次声明两个变量,步进段 `left++, right--` 一次动两个。注意逗号只能用在初始化段和步进段,条件段只能是**一个** boolean 表达式。
> [!WARNING]
> 数组下标从 0 开始,最后一个下标是 `length - 1`。条件写成 `i <= arr.length` 会多跑一轮,运行时抛越界:
>
> ```text
> Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 5
> at ForDemo.main(ForDemo.java:6)
> ```
>
> 看到 `Index 5 out of bounds for length 5`,第一反应就是检查循环条件里的 `<` 是不是被写成了 `<=`。
## 增强 for:for-each
遍历数组或集合时,我们常常只关心「里面的每个元素」,不关心下标是多少。Java 5 引入的增强 for(也叫 for-each)就是为这个场景生的。
```java
public class ForEachDemo {
public static void main(String[] args) {
int[] scores = {88, 92, 75, 60};
for (int s : scores) { // 依次把每个元素赋给 s
System.out.println(s);
}
}
}
for (int s : scores) 读作「对于 scores 里的每一个 int 元素 s」。它不用写下标、不用写长度、不会越界,代码干净还不容易出错。
冒号左边是「元素的类型 + 变量名」,右边是要遍历的数组或集合。类型必须和元素类型对得上,写错了编译就拦:
ForEachDemo.java:5: error: incompatible types: int cannot be converted to String
for (String s : scores) {
^
```bash
### 增强 for 背后等价于什么
它不是新魔法,编译器会把它翻译成你熟悉的写法。遍历数组时,等价于:
```java
for (int idx = 0; idx < scores.length; idx++) {
int s = scores[idx]; // 把元素复制给 s
System.out.println(s);
}
遍历集合时则等价于迭代器写法:
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String name = it.next();
System.out.println(name);
}
```bash
知道这层翻译关系,后面那些「为什么改不了元素」「为什么删元素会崩」的疑问就都有答案了。
### 遍历集合
增强 for 对集合同样适用,写法一模一样:
```java
import java.util.List;
public class ListEachDemo {
public static void main(String[] args) {
List<String> names = List.of("小明", "小红", "小刚");
for (String name : names) {
System.out.println("你好," + name);
}
}
}
List.of(...) 是 Java 9 加的集合工厂方法,一行造出一个不可变列表,练手很方便。
从 Java 10 起还能用 var 让编译器推断元素类型:
for (var name : names) { // var 自动推断为 String
System.out.println(name);
}
```bash
元素类型名很长时(比如 `Map.Entry<String, List<Integer>>`),`var` 能省掉一大截。类型短的时候还是写清楚更好读。
## 两种 for 怎么选
- 普通 for:需要知道当前是第几个、要改元素、要跳着取。
- 增强 for:单纯遍历、只读不写、不关心位置。
| 对比项 | 普通 for | 增强 for |
|--------|----------|----------|
| 能拿到下标 | 能 | 不能 |
| 能倒着遍历 | 能 | 不能,只能从头到尾 |
| 能跳着遍历 | 能,改步进即可 | 不能,一个不落 |
| 能按位置改数组元素 | 能 | 不能 |
| 能中途删集合元素 | 能,但要小心下标 | 不能,会抛异常 |
| 会不会越界 | 会,条件写错就崩 | 不会 |
| 代码长度 | 长 | 短 |
> [!TIP]
> 只是从头到尾读一遍元素,用增强 for 最省心。需要改元素、需要下标、需要倒着遍历时,再用普通 for。
### 为什么增强 for 改不了元素
看这段代码,直觉上应该把所有分数翻倍,实际跑完数组纹丝不动:
```java
int[] scores = {88, 92, 75, 60};
for (int s : scores) {
s = s * 2; // 只改了副本 s,数组里的值没动
}
System.out.println(scores[0]); // 还是 88
原因看前面那段等价翻译:int s = scores[idx]; 是把元素复制给了 s。改 s 只是改这个临时副本,跟数组里那个格子没关系。
要真改,必须通过下标写回去:
for (int i = 0; i < scores.length; i++) {
scores[i] = scores[i] * 2; // 直接往数组格子里写
}
```bash
> [!NOTE]
> 元素是对象时情况微妙一点。`for (Student s : list)` 里的 s 是引用副本,指向的还是同一个对象,所以 `s.setName("新名字")` 能改到。但 `s = new Student()` 依然只是让副本指向别处,集合里的元素不会被替换。
### 边遍历边删会崩
增强 for 遍历集合时删元素,运行到一半就炸:
```java
import java.util.ArrayList;
import java.util.List;
List<String> names = new ArrayList<>(List.of("小明", "小红", "小刚"));
for (String name : names) {
if ("小红".equals(name)) {
names.remove(name); // 危险操作
}
}
Exception in thread "main" java.util.ConcurrentModificationException
at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1013)
at java.base/java.util.ArrayList$Itr.next(ArrayList.java:967)
at ListEachDemo.main(ListEachDemo.java:8)
```text
`ConcurrentModificationException` 直译是「并发修改异常」,但这里根本没多线程。它的真实含义是:迭代器发现集合在遍历途中被人从外部改了结构,为了避免读到错乱数据,主动喊停。
想边遍历边删,用集合自带的 `removeIf`:
```java
names.removeIf(name -> "小红".equals(name));
removeIf 是 Java 8 加的方法,一行搞定,内部处理好了删除时的下标问题。Lambda 表达式的写法第 47 章会展开讲。
嵌套 for
for 里还能套 for,常用来画图形或处理二维数据。
public class StarDemo {
public static void main(String[] args) {
for (int i = 1; i <= 3; i++) { // 外层:行
for (int j = 1; j <= i; j++) { // 内层:每行几个
System.out.print("*");
}
System.out.println(); // 换行
}
}
}
```text
外层每走一圈,内层就完整跑一轮。这是后面处理二维数组和矩阵的基础。
顺着执行流走一遍:
- i = 1 时,内层条件 `j <= 1`,只跑 1 轮,打 1 个星号,然后换行。
- i = 2 时,内层跑 2 轮,打 2 个星号,换行。
- i = 3 时,内层跑 3 轮,打 3 个星号,换行。
输出是个三角形:
```text
*
**
***
内层的 j 每次进外层新一轮时都会重新初始化成 1。这就是把 j 声明在内层 for 里的好处——它自动归零,不用你手动重置。
再看个经典的九九乘法表,思路完全一样:
for (int i = 1; i <= 9; i++) {
for (int j = 1; j <= i; j++) {
System.out.printf("%d×%d=%-3d", j, i, i * j); // %-3d 左对齐占 3 位
}
System.out.println();
}
```bash
`%-3d` 里的减号表示左对齐,3 表示至少占 3 个字符宽。这样每一列都能对齐,不会因为个位数和两位数错开。
> [!WARNING]
> 嵌套循环的执行次数是相乘的。外层 1000 次、内层 1000 次,循环体就要跑一百万次。写三层嵌套前先算算这个乘积,很多「程序卡住」其实不是死循环,是次数太多在硬算。
## 历史兼容:Java 5 之前怎么遍历
增强 for 是 Java 5(2004 年)才有的。在那之前,遍历集合只能手写迭代器:
```java
// 历史兼容,不推荐:Java 5 之前的写法,还容易忘记泛型
List names = new ArrayList(); // 没有泛型,取出来是 Object
names.add("小明");
for (Iterator it = names.iterator(); it.hasNext(); ) {
String name = (String) it.next(); // 必须手动强制转型
System.out.println(name);
}
三个毛病:啰嗦、要手动转型、转错了运行时才崩。增强 for 加上泛型把这三样一起解决了。现在看到老项目里这种写法,知道它是什么意思即可,新代码不要跟着写。
常见疑问
问:i++ 和 ++i 放在步进段有区别吗?
在 for 的步进段里没有区别。这一段的返回值没人接收,两者都只是让 i 加 1。区别只在把它当表达式用的时候,比如 int a = i++ 和 int a = ++i 结果不同。步进段随便写哪个,团队统一 i++ 就好。
问:增强 for 能同时拿到下标吗?
不能,这是它的设计取舍——换来的是简洁和不越界。真需要下标就有两条路:一是老老实实用普通 for;二是在增强 for 外面自己维护一个计数器,每轮手动加 1。后者不如前者清爽,多数时候直接换回普通 for 更好。
问:遍历数组时,arr.length 会不会每轮都重新计算,影响性能?
不用担心。数组的 length 是个字段不是方法,读它几乎不花时间,JIT 编译器还会进一步优化掉重复读取。为了这个把 arr.length 提取到循环外的变量,属于没必要的微优化,反而让代码变长。真正需要提前算的是那些每轮都要重新计算的耗时表达式。