6.2 数组索引与遍历
前置知识:数组声明、索引边界和循环
预计时间:20 分钟
完成标志:能根据是否需要索引、修改组件或倒序访问来选择遍历方式
scores 有 85、92、78、95、88 五个成绩。手写五条输出还能应付,但数组一变长,就该让循环负责索引。
1. 索引循环给出位置和值
int[] scores = {85, 92, 78, 95, 88};
for (int index = 0; index < scores.length; index++) {
System.out.println("第 " + (index + 1)
+ " 个人的分数:" + scores[index]);
}输出:
第 1 个人的分数:85
第 2 个人的分数:92
第 3 个人的分数:78
第 4 个人的分数:95
第 5 个人的分数:88循环条件使用 index < scores.length。当索引等于长度时,循环结束,不会执行越界访问。空数组的长度为 0,第一次检查条件就失败,因此循环体执行 0 次。
需要原地修改组件时也用索引:
for (int index = 0; index < scores.length; index++) {
scores[index] = Math.min(100, scores[index] + 5);
}这里假设输入成绩已经校验为 0 到 100,再把加分结果限制到 100。若业务允许超过 100,应删除这个规则;代码不能替业务偷偷决定边界。
2. 增强 for 适合只关心值
for (int score : scores) {
System.out.println("分数:" + score);
}增强 for 按顺序取出每个组件的值,并把它赋给循环变量 score。它不会直接暴露索引,适合读取、统计和调用只需元素值的方法。
给循环变量重新赋值不会替换数组组件:
int[] values = {1, 2, 3};
for (int value : values) {
value = 0;
}
System.out.println(java.util.Arrays.toString(values));
// [1, 2, 3]说“循环变量是副本”时要留意组件类型:
- 对
int[],复制的是整数值。 - 对
String[]、Person[]等引用类型数组,复制的是引用值。重新指定循环变量不会替换数组组件,但通过该引用修改可变对象,数组中的引用仍会观察到同一个对象。
Java 的参数传递也是同样规律:始终按值传递。把数组传给方法时,复制的是数组引用值。方法可以通过复制的引用修改组件,却不能仅靠给参数重新赋值来替换调用方变量。
static void changeFirst(int[] values) {
values[0] = 99;
}
static void replaceLocally(int[] values) {
values = new int[]{7, 8, 9}; // 只改变参数变量
}调用 changeFirst(original) 会修改 original[0];调用 replaceLocally(original) 不会让 original 指向新数组。
3. 倒序与相邻访问要先推导边界
倒序输出:
for (int index = scores.length - 1; index >= 0; index--) {
System.out.println(scores[index]);
}空数组时初值是 -1,条件立即失败。这里的索引是 int;若换成无符号索引类型的语言,倒序写法可能需要调整。
比较相邻元素时,循环只走到倒数第二个位置:
for (int index = 0; index + 1 < scores.length; index++) {
long difference = (long) scores[index + 1] - scores[index];
System.out.println(difference);
}不要先背循环模板。先写出循环体会访问哪些索引,再反推使所有访问合法的条件。
4. 统计时明确空输入契约
求和对空数组可以自然返回 0:
static long sum(int[] values) {
long total = 0L;
for (int value : values) {
total = Math.addExact(total, value);
}
return total;
}求平均值却要决定空数组的含义:
static double average(int[] values) {
if (values.length == 0) {
throw new IllegalArgumentException("empty values");
}
return (double) sum(values) / values.length;
}若 values 允许为 null,还要在契约中说明是拒绝、当作空数组,还是使用其他含义。上面的实现让 null 产生 NullPointerException,适合把 null 视为调用方编程错误的内部 API;公共 API 常用 Objects.requireNonNull(values, "values") 给出更明确的消息。
5. 选择遍历方式
| 需求 | 推荐写法 | 原因 |
|---|---|---|
| 只读取每个值 | 增强 for | 不暴露无用索引 |
| 修改数组组件 | 索引 for | 可写 values[index] |
| 需要位置或相邻值 | 索引 for | 位置参与计算 |
| 倒序访问 | 索引 for | 明确控制起点和方向 |
| 同时处理多数组对应位置 | 索引 for | 先验证长度关系 |
流式 API 也能处理数组,但在掌握循环、边界和副作用之前,不应把简单遍历包装成更难调试的表达式。
语言差异窗口
Python 的 enumerate 同时给出索引和值:
scores = [85, 92, 78, 95, 88]
for index, score in enumerate(scores):
print(f"第 {index + 1} 个人的分数:{score}")C++17 的 range-based for 可按值或按引用绑定。for (int value : values) 修改局部值,for (int& value : values) 可修改原组件。这与 Java 增强 for 的引用模型不同,不能只按外观类比。
常见故障
| 现象 | 首先检查 |
|---|---|
| 最后一轮越界 | 是否写成 index <= values.length |
| 增强 for 修改无效 | 是否只给循环变量重新赋值 |
| 空数组求平均得到异常或非数 | 是否定义了空输入策略 |
| 方法修改了原数组 | 是否通过复制的引用写了组件 |
| 两数组同步遍历越界 | 是否先验证长度或按较短长度处理 |
离开这一页前
用空数组、单元素数组和三元素数组分别走一遍正序、倒序与相邻元素循环。需要修改组件时指出索引写回的位置;使用增强 for 时则说明循环变量复制的是基本值还是引用值。求平均值还必须先决定空输入和 null 的契约。