Mr.Mou @ ShiShi AP Center

AP CSA 4.4:数组遍历

上一课我们学习了如何创建数组,以及如何通过 index 访问单个元素。这一课要解决一个更实际的问题:

如果数组里有 100 个元素,难道要一个一个写出 array[0]、array[1]、array[2] 吗?

当然不需要。我们可以使用循环(loop),让程序自动访问数组中的元素,并对它们进行处理。

数组遍历(array traversal)就是使用循环,按顺序访问数组中的全部元素,或其中一部分元素。理解本课的关键,是掌握 indexed loop 和 enhanced for loop 的区别。

核心知识点

1. 什么是数组遍历(Array Traversal)?

数组遍历就是使用循环,按顺序访问数组中的元素。

例如,我们想打印数组里的所有成绩:

int[] scores = {95, 88, 92, 76, 100};

如果没有循环,就需要逐个写:

System.out.println(scores[0]);
System.out.println(scores[1]);
System.out.println(scores[2]);
System.out.println(scores[3]);
System.out.println(scores[4]);

如果数组有 100 个元素,这样写就非常麻烦。

使用循环,我们只需要写一次操作,让循环自动访问每个元素。

遍历时可以进行不同的操作:

遍历不一定意味着修改数据。它的核心是按顺序访问元素。

2. 使用 Indexed for Loop 遍历数组

最常见的方式是使用带索引的 for loop,也叫 indexed for loop。

int[] scores = {95, 88, 92, 76, 100};

for (int i = 0; i < scores.length; i++)
{
    System.out.println(scores[i]);
}

输出:

95
88
92
76
100

理解这个循环的三个部分:

部分 代码 作用
初始化(initialization) int i = 0 从第一个 index 开始
条件(condition) i < scores.length 只要 index 还在有效范围内,就继续循环
更新(update) i++ 每次循环后,index 加 1

每一轮循环中,i 都会改变:

循环轮次 i 访问的元素
第 1 轮 0 scores[0]
第 2 轮 1 scores[1]
第 3 轮 2 scores[2]
第 4 轮 3 scores[3]
第 5 轮 4 scores[4]

当 i 变成 5 时,i < scores.length 不再成立,循环结束。

2.1 为什么条件是 i < scores.length?

假设数组长度为 5:

因此:

i < scores.length

等价于:

i <= scores.length - 1

两种条件都能访问全部元素,但 AP 代码中通常使用 i < array.length。

记住:数组遍历的标准写法是从 0 开始,条件使用 < array.length。

2.2 使用 while Loop 完成相同的遍历

也可以使用 while loop:

int i = 0;

while (i < scores.length)
{
    System.out.println(scores[i]);
    i++;
}

它与上面的 for loop 做的是同一件事:

  1. 从 index 0 开始。
  2. 检查 index 是否小于数组长度。
  3. 访问当前元素。
  4. i++,进入下一个 index。

两种方式都可以遍历数组。由于遍历数组通常是简单的计数循环,教材更常使用 for loop。

注意: while loop 的 i++ 必须正确执行,否则可能反复访问同一个元素,造成无限循环。

3. 控制遍历范围

遍历不一定要从头到尾访问全部元素。我们也可以根据题目要求,只访问数组中的一部分元素,或者改变访问顺序。

3.1 遍历前五个元素

假设数组可能有很多元素,但我们只想处理前五个:

for (int i = 0; i < values.length && i < 5; i++)
{
    values[i] = values[i] * 2;
}

这里有两个条件:

两个条件必须同时成立,循环才会继续。

如果数组只有 3 个元素,循环就只处理这 3 个元素,不会访问不存在的第 4、5 个元素。

3.2 从数组中间遍历到末尾

for (int i = values.length / 2; i < values.length; i++)
{
    values[i] = values[i] * 2;
}

假设:

int[] values = {-20, -15, 2, 8, 16, 33};

数组长度为 6,因此:

values.length / 2

等于 3。

循环从 index 3 开始,访问并修改:

Index 0 1 2 3 4 5
Original -20 -15 2 8 16 33
After loop -20 -15 2 16 32 66

注意:循环只处理了后半部分,并没有修改前面的三个元素。

3.3 每隔一个元素遍历

for loop 的更新部分不一定只能使用 i++。

例如:

for (int i = 0; i < values.length; i += 2)
{
    values[i] = values[i] * 2;
}

i += 2 等价于 i = i + 2。

如果数组是:

int[] values = {-20, -15, 2, 8, 16, 33};

访问的 index 是:

0 → 2 → 4

因此只有这三个位置会被修改:

{-40, -15, 4, 8, 32, 33}

追踪这类题目时,不要默认 index 每次只增加 1。一定要看清楚更新表达式。

3.4 反向遍历

有时题目要求从最后一个元素开始访问:

for (int i = values.length - 1; i >= 0; i--)
{
    System.out.println(values[i]);
}

这里有三个重要变化:

如果数组是:

int[] values = {10, 20, 30, 40};

访问顺序是:

index:  3 → 2 → 1 → 0
value: 40 → 30 → 20 → 10

4. Off-by-One Errors:数组遍历中的常见边界错误

这是 AP 代码追踪和错误分析中非常重要的内容。

Off-by-one error 指循环边界设置错误,导致少访问一个元素,或者访问了数组范围之外的位置。

4.1 最常见的错误:使用 <=

错误代码:

int[] scores = {10, 9, 8, 7};

for (int i = 0; i <= scores.length; i++)
{
    System.out.println(scores[i]);
}

数组长度是 4,有效 index 是 0、1、2、3。

但是,条件 i <= scores.length 允许 i 等于 4。

当程序执行 scores[4] 时,就会产生:

ArrayIndexOutOfBoundsException

正确写法:

for (int i = 0; i < scores.length; i++)
{
    System.out.println(scores[i]);
}

4.2 另一个错误:从 index 1 开始

for (int i = 1; i < scores.length; i++)
{
    System.out.println(scores[i]);
}

这段代码不会产生越界异常,但会跳过第一个元素 scores[0]。

所以要区分:

循环条件 结果
i = 0; i < scores.length; i++ 正确访问全部元素
i = 1; i < scores.length; i++ 不会越界,但漏掉第一个元素
i = 0; i <= scores.length; i++ 最后会越界

4.3 反向遍历时,更新方向也不能写错

错误:

for (int i = scores.length - 1; i >= 0; i++)
{
    System.out.println(scores[i]);
}

虽然初始 index 和条件都正确,但 i++ 会让 index 越来越大,最终超出数组范围。

正确写法:

for (int i = scores.length - 1; i >= 0; i--)
{
    System.out.println(scores[i]);
}

4.4 追踪循环时的检查顺序

遇到数组遍历题,建议依次检查:

  1. Start:index 从哪里开始?
  2. Condition:循环什么时候停止?
  3. Update:每轮 index 如何变化?
  4. Access:循环体实际访问哪个元素?

不要只看循环条件,也不要忽略 i++、i-- 或 i += 2。

5. 数组作为 Method 参数:遍历后修改原数组

上一课我们学过,数组是对象,传入 method 的是数组引用的副本。两个引用仍然指向同一个数组对象。

这一点在数组遍历中很重要,因为 method 可以通过循环修改原数组中的元素。

例如:

public static void multAll(int[] values, int amt)
{
    for (int i = 0; i < values.length; i++)
    {
        values[i] = values[i] * amt;
    }
}

调用:

int[] numArray = {2, 6, 7, 12, 5};

multAll(numArray, 2);

执行后,原数组变成:

{4, 12, 14, 24, 10}

原因是 values 和 numArray 指向同一个数组对象。method 通过 values[i] 修改元素,原数组也随之改变。

注意: 这里改变的是数组对象中的元素,不是把调用者的数组变量重新指向另一个数组。

6. Enhanced for Loop:增强型 for 循环

除了 indexed for loop,Java 还有一种更简洁的循环,叫作增强型 for 循环(enhanced for loop),也叫 for-each loop。

它不需要 index,也不需要写 [],而是依次把数组中的元素交给一个循环变量。

6.1 语法

for (type variable : arrayName)
{
    // use variable
}

可以读作:

对于 arrayName 中的每一个元素,把它交给 variable。

例如:

int[] scores = {95, 88, 92, 76};

for (int score : scores)
{
    System.out.println(score);
}

输出:

95
88
92
76

再看一个 String 数组:

String[] names = {"Jamal", "Emily", "Mateo"};

for (String name : names)
{
    System.out.println(name);
}

注意:循环变量的类型必须与数组元素类型匹配。

6.2 每轮循环,变量得到的是元素的 Copy

这是 enhanced for loop 最重要的概念,也是 AP 4.4 的核心考点。

College Board 明确指出:每次循环时,enhanced for loop variable 会得到当前数组元素的一个 copy。

例如:

int[] values = {6, 2, 1, 7};

for (int val : values)
{
    val++;
}

很多学生会认为数组变成:

{7, 3, 2, 8}

但实际上并没有改变。

原因是:

如果之后再次打印数组,结果仍然是:

6 2 1 7

记住:给 enhanced for loop variable 重新赋值,不会改变数组中保存的值。

6.3 什么时候使用 Enhanced for Loop?

它特别适合:

以下情况不适合使用 enhanced for loop:

需求 更合适的循环
需要知道当前 index Indexed for loop
只访问前五个或某一段元素 Indexed for loop
从后往前访问 Indexed for loop
修改 int、double 等基本类型数组中的元素 Indexed for loop
需要把数组中的某个位置替换成新对象 Indexed for loop

7. Array of Objects:对象数组中的 Enhanced for Loop

这里是本课最容易混淆、也最值得理解的地方。

前面我们学过:对象数组中的元素保存的是对象引用(object references)。

例如:

Student[] students = {
    new Student("Mia"),
    new Student("Leo"),
    new Student("Eva")
};

当我们使用 enhanced for loop 遍历它:

for (Student student : students)
{
    student.printInfo();
}

每轮循环中的 student 得到的是当前数组元素所保存的引用副本。

这个引用副本仍然指向原来的 Student 对象。

因此,我们可以通过它调用 mutator method(修改器方法),改变对象的属性。

7.1 可以修改对象的属性

假设 Student 类有一个 addBonus 方法,可以增加学生的分数:

for (Student student : students)
{
    student.addBonus(5);
}

这段代码会逐个访问数组中的 Student 对象,并通过 addBonus(5) 修改每个对象的分数。

虽然对象的属性发生了变化,但数组中的每个元素仍然引用原来的 Student 对象。

换句话说,我们修改的是对象的内部状态,而不是将数组元素指向另一个对象。

7.2 不能通过循环变量替换数组中的对象

看下面的代码:

for (Student student : students)
{
    student = new Student("New Student");
}

这不会把 students 数组中的每个位置替换成新的 Student 对象。

原因是:

如果真的需要替换数组中的某个对象,应该使用 index:

for (int i = 0; i < students.length; i++)
{
    students[i] = new Student("New Student");
}

7.3 两种操作的区别

操作 能否通过 enhanced for loop 完成? 原因
读取对象属性 可以 循环变量引用着原对象
调用 setter 修改对象属性 可以 方法作用于原对象
把循环变量重新赋值为新对象 不可以替换数组元素 只改变局部变量的引用
替换数组中某个位置的对象 不可以直接完成 需要通过 index 修改数组元素

本课最重要的理解:

对于基本类型,循环变量得到的是值的副本;对于对象数组,循环变量得到的是引用的副本。

引用副本仍然可以访问和修改原对象的属性,但不能直接替换数组中保存的引用。

8. 两种循环如何互相改写?

College Board 的 4.4.A.6 明确要求:使用 enhanced for loop 编写的遍历代码,可以改写成 indexed for loop 或 while loop。

例如,使用 enhanced for loop 打印所有成绩:

for (int score : scores)
{
    System.out.println(score);
}

等价的 indexed for loop:

for (int i = 0; i < scores.length; i++)
{
    System.out.println(scores[i]);
}

等价的 while loop:

int i = 0;

while (i < scores.length)
{
    System.out.println(scores[i]);
    i++;
}

三段代码都按照数组原有顺序访问每个元素,并打印结果。

区别在于:

考试时要能够读懂三种写法,并判断它们是否完成了相同的遍历。

9. 遍历数组时的另一个常见错误:过早 Return

Runestone 还通过一个查找姓名的例子,提醒我们:不要在循环还没有检查完所有元素时,就提前返回“没有找到”。

错误代码:

public int findString(String target)
{
    for (int i = 0; i < arr.length; i++)
    {
        if (arr[i].equals(target))
        {
            return i;
        }
        else
        {
            return -1;
        }
    }

    return -1;
}

问题在于:

如果第一个元素不匹配,程序就立即执行 return -1,后面的元素根本没有机会检查。

正确写法:

public int findString(String target)
{
    for (int i = 0; i < arr.length; i++)
    {
        if (arr[i].equals(target))
        {
            return i;
        }
    }

    return -1;
}

现在程序会:

  1. 逐个检查数组元素。
  2. 找到匹配项时,立即返回它的 index。
  3. 只有循环结束、确认所有元素都检查过之后,才返回 -1。

这种逐个检查元素的查找方式叫作线性查找(linear search)。

记住:找到了可以提前 return;没有找到,必须等遍历结束后才能 return。

10. AP Exam Focus:六个核心考点

College Board 将本课归在 4.4.A:使用代码遍历一维数组,并确定遍历的结果。

AP 知识点 必须掌握的内容
4.4.A.1 数组遍历是使用循环访问数组中的全部元素,或按顺序访问其中一部分元素。
4.4.A.2 使用 indexed for loop 或 while loop 遍历时,需要通过 index 访问元素。
4.4.A.3 Enhanced for loop 每轮将一个元素的副本赋给循环变量,不需要使用 index。
4.4.A.4 给 enhanced for loop 的循环变量重新赋值,不会改变数组中存储的值。
4.4.A.5 如果数组保存对象引用,可以通过 enhanced for loop 变量调用方法修改对象属性;但数组中保存的对象引用不会改变。
4.4.A.6 Enhanced for loop 可以改写成 indexed for loop 或 while loop。

AP 考试技能

本课对应三项建议技能:

因此,不能只会背循环语法。还需要能追踪每一轮循环、判断数组最终状态,并修复循环边界或逻辑错误。

11. Common Mistakes:常见错误

常见错误 正确理解
使用 i <= array.length 遍历全部元素 应使用 i < array.length,否则最后会越界
从 i = 1 开始,以为可以访问全部元素 不会越界,但会漏掉 index 0
反向遍历时使用 i++ 应使用 i--,否则 index 会越来越大
忘记在 while loop 中更新 index 可能一直访问同一个元素,形成无限循环
认为 enhanced for loop 中 val++ 会修改原数组 只修改循环变量的副本
认为 enhanced for loop 完全不能修改对象 可以通过循环变量调用 mutator method 修改对象属性
认为 student = new Student(...) 会替换数组中的对象 只改变局部循环变量;替换数组元素需要 index
在查找循环中,第一次不匹配就 return -1 应遍历完所有元素后再返回 -1

12. Mini Practice:小练习

Practice 1:理解数组遍历

下面哪项最准确地描述了 array traversal?

A. 只能访问数组中的第一个元素。

B. 使用循环访问数组中的全部元素,或按顺序访问其中一部分元素。

C. 必须修改数组中的每一个元素。

答案:B

解释:遍历的核心是使用循环访问元素,可以读取、打印、比较或修改元素,并不要求一定修改数据。

Practice 2:循环边界

下面哪个循环能够正确访问数组 scores 的全部元素?

A.

for (int i = 0; i <= scores.length; i++)

B.

for (int i = 1; i < scores.length; i++)

C.

for (int i = 0; i < scores.length; i++)

答案:C

解释:A 最后会访问 scores[scores.length],产生越界异常;B 会跳过第一个元素;C 才能完整访问所有元素。

Practice 3:部分遍历与代码追踪

int[] nums = {2, 4, 6, 8, 10, 12};

for (int i = 0; i < nums.length && i < 4; i++)
{
    nums[i] *= 2;
}

最终数组是什么?

A. {4, 8, 12, 16, 10, 12}

B. {4, 8, 12, 16, 20, 24}

C. {2, 4, 6, 8, 20, 24}

答案:A

解释:循环只处理 index 0、1、2、3。后面两个元素不会被修改。

Practice 4:Enhanced for loop

int[] values = {3, 5, 7};

for (int value : values)
{
    value *= 2;
}

System.out.println(values[0] + " " + values[1] + " " + values[2]);

输出是什么?

A. 6 10 14

B. 3 5 7

C. 5 7 9

答案:B

解释:value 每轮得到的是数组元素的副本。value *= 2 不会修改数组中的原始值。

Practice 5:对象数组与 Enhanced for loop

假设 Student 类有 addBonus(int amount) 方法和一个 constructor。

执行下面代码:

for (Student student : students)
{
    student.addBonus(5);
}

哪项描述正确?

A. 每个 Student 对象的属性可以被修改,数组中保存的对象引用保持不变。

B. 循环变量 student 被重新赋值,因此数组中的所有对象都会被替换。

C. Enhanced for loop 不能用于对象数组。

答案:A

解释:循环变量保存的是对象引用的副本,但仍然指向原对象。调用 mutator method 可以修改原对象的属性。

Practice 6:查找代码中的错误

下面的方法想在数组中查找 target。如果找到,返回 index;如果没有找到,返回 -1。

public int findString(String target)
{
    for (int i = 0; i < arr.length; i++)
    {
        if (arr[i].equals(target))
        {
            return i;
        }
        else
        {
            return -1;
        }
    }

    return -1;
}

问题在哪里?

A. return i 应该放在循环外面。

B. return -1 放在 else 中,会在第一次不匹配时提前结束循环。

C. arr.length 应该写成 arr.length - 1。

答案:B

解释:只有检查完所有元素仍然没有找到,才能返回 -1。应删除 else 中的 return -1,保留循环结束后的 return -1。

Unit 4.4 核心词汇(Vocabulary)

Vocabulary 中文理解
array traversal 数组遍历;使用循环访问数组元素
traverse 遍历;按顺序访问元素
iteration 迭代;循环执行的一轮
indexed for loop 索引式 for 循环;使用 index 访问元素
enhanced for loop 增强型 for 循环;依次取得数组元素,不需要 index
for-each loop 增强型 for 循环的另一种名称
loop variable 循环变量;每轮循环中接收当前元素的变量
off-by-one error 边界错误;循环多访问或少访问一个位置
mutator method 修改器方法;用于修改对象属性的方法
linear search 线性查找;逐个检查元素,寻找目标值

下一课预告: 4.4 学会了如何遍历数组。4.5 将进一步学习如何利用数组遍历实现常见算法,例如求最大值、计算平均值、统计符合条件的元素,以及查找重复值。