AP CSA 4.11:二维数组的创建与访问
前面我们已经学过一维数组(1D array):一个数组可以保存许多相同类型的元素。
如果学校要记录三名学生在四次小测中的成绩,我们当然可以使用多个一维数组,但数据本身其实更像一张表格:每一行对应一名学生,每一列对应一次小测。
这就是二维数组(two-dimensional array / 2D array)特别适合解决的问题。
理解二维数组的关键:它不是一种完全陌生的数据结构,而是“数组里面还有数组”。先选哪一行,再选这一行中的哪个元素。
核心知识点
1. 先看一个问题:二维数组到底是什么?
假设有这样一张成绩表:
| Row index / 学生 | Column 0 / Quiz 1 | Column 1 / Quiz 2 | Column 2 / Quiz 3 | Column 3 / Quiz 4 |
|---|---|---|---|---|
| Row 0 | 88 | 92 | 95 | 80 |
| Row 1 | 75 | 90 | 82 | 91 |
| Row 2 | 100 | 84 | 96 | 89 |
请先回答:第三名学生的第二次小测成绩是多少?
答案是 84。
如果把这张表保存为二维数组 scores,就可以这样访问:
scores[2][1]
为什么是 [2][1],而不是 [3][2]?
因为 Java 的数组索引(index)从 0 开始。第三行的 index 是 2;第二列的 index 是 1。
以后看到 array[row][col],就按照这个顺序思考:先找行,再找列。
2. 二维数组是“数组的数组”(Array of Arrays)
一维数组可以这样写:
int[] quizScores = {88, 92, 95, 80};
它保存的是一个学生的四次小测成绩。
如果要保存三个学生的成绩,就可以让外层数组保存三个一维数组:
int[][] scores = {
{88, 92, 95, 80},
{75, 90, 82, 91},
{100, 84, 96, 89}
};
这段代码可以从两个角度理解。
从表格角度:
col 0 col 1 col 2 col 3
row 0 88 92 95 80
row 1 75 90 82 91
row 2 100 84 96 89
从 Java 对象角度:
scores(外层数组)
|
+-- [0] --> int[] {88, 92, 95, 80}
|
+-- [1] --> int[] {75, 90, 82, 91}
|
+-- [2] --> int[] {100, 84, 96, 89}
也就是说:
scores是一个二维数组变量,保存着外层数组对象的引用。- 外层数组有 3 个元素;每个元素保存着一行一维数组的引用。
- 每行的一维数组包含 4 个
int元素。
因此,int[][] 并不是把 int[] 的概念推翻重来。二维数组的每一行,本身就是一个一维数组。
这也是为什么后面可以写 scores[0].length:scores[0] 取得的就是第 0 行这个一维数组。
2.1 行(Row)与列(Column)
| 名称 | 在表格中的方向 | 在代码中的位置 |
|---|---|---|
| Row(行) | 水平方向的一整排 | scores[row][col] 中第一个 index |
| Column(列) | 垂直方向的一整列 | scores[row][col] 中第二个 index |
3. 声明与创建二维数组(Declaration and Creation)
3.1 声明:两个 []
int[][] scores;
这只是声明一个二维 int 数组变量,还没有创建数组对象。与 4.3 的一维数组一样,声明变量和创建数组是两回事。
3.2 使用 new 创建
scores = new int[3][4];
或者合成一行:
int[][] scores = new int[3][4];
new int[3][4] 的含义:
| 代码部分 | 意义 |
|---|---|
int |
最终每个单元格保存 int |
第一个 [3] |
3 行(rows) |
第二个 [4] |
每行 4 个元素,也就是 4 列(columns) |
| 整个表达式 | 创建一个 3 行 × 4 列 的矩形二维数组 |
总共可以保存多少个 int 值?
3 × 4 = 12 个。
请特别注意:new int[3][4] 不等于 new int[4][3]。前者是 3 行 4 列;后者是 4 行 3 列。两者都有 12 个元素,但形状不同,合法索引范围也不同。
3.3 二维数组的长度也固定
使用 new 创建后,每个数组对象的长度都固定。这包括外层数组的长度,以及每一行一维数组的长度。
对于 AP CSA,本课按照矩形二维数组理解:所有行的列数相同。
考试范围: College Board 明确排除了非矩形二维数组(nonrectangular / ragged arrays)。Java 语言本身允许每行长度不同,但我们无需把这种特殊情况作为 AP 4.11 的学习重点。
4. 使用 new 创建后的默认值(Default Values)
像一维数组一样,二维数组使用 new 创建时,每个单元格都会自动获得对应类型的默认值。
int[][] scores = new int[2][3];
刚创建完时:
| col 0 | col 1 | col 2 | |
|---|---|---|---|
| row 0 | 0 | 0 | 0 |
| row 1 | 0 | 0 | 0 |
其他类型的默认值:
| 数组类型 | 每个单元格的默认值 |
|---|---|
int[][] |
0 |
double[][] |
0.0 |
boolean[][] |
false |
String[][]、Student[][] 等引用类型 |
null |
例如:
String[][] seats = new String[2][3];
创建后有两行、每行三个位置,但每个单元格都是 null,并不是空字符串 ""。
类似地:
Student[][] students = new Student[2][2];
这会创建二维数组及其行数组,但不会自动创建四个 Student 对象。四个单元格初始都是 null。如果需要学生对象,还要单独调用 constructor:
students[0][0] = new Student("Mia");
和 4.3 的对象数组原则完全相同:创建保存对象引用的数组,不等于创建这些对象。
5. 使用初始化列表(Initializer Lists)
如果已经知道每个单元格应该保存什么值,可以直接使用嵌套的大括号 {}。
int[][] scores = {
{88, 92, 95, 80},
{75, 90, 82, 91},
{100, 84, 96, 89}
};
观察大括号的层次:
- 最外层
{ ... }:包含整个二维数组。 - 第一组内层
{88, 92, 95, 80}:第 0 行。 - 第二组内层
{75, 90, 82, 91}:第 1 行。 - 第三组内层
{100, 84, 96, 89}:第 2 行。
Java 会根据初始化列表确定数组大小:3 行,每行 4 列。
也可以用对象引用类型:
String[][] seatingChart = {
{"Mia", "Leo"},
{"Eva", "Noah"},
{"Ava", "Liam"}
};
这表示 3 行 × 2 列。例如 seatingChart[1][0] 的值是 "Eva"。
两种创建方法怎么选?
| 创建方式 | 示例 | 适合情况 |
|---|---|---|
| 指定尺寸,用默认值填充 | new int[3][4] |
已知行数、列数,稍后再逐个赋值 |
| 使用初始化列表 | {{1, 2}, {3, 4}} |
创建时就知道要保存的数据 |
6. 读取和修改元素(Get and Set Values)
二维数组使用两组方括号:
arrayName[row][col]
第一个 index 是行,第二个 index 是列,而且都从 0 开始。
继续使用同一张成绩表:
int[][] scores = {
{88, 92, 95, 80},
{75, 90, 82, 91},
{100, 84, 96, 89}
};
6.1 读取一个元素
int score = scores[2][1];
思考顺序:
scores[2]:先找到 index 2,也就是第三行{100, 84, 96, 89}。[1]:再从这一行里找 index 1,也就是第二个元素。- 得到
84。
所以 score 的值是 84。
另一个例子:
System.out.println(scores[1][3]);
输出:
91
因为 index 1 是第二行,index 3 是第四列。
6.2 修改一个元素
scores[1][3] = 99;
这会把第二名学生的第四次小测成绩从 91 改为 99。
更新后的表格:
| col 0 | col 1 | col 2 | col 3 | |
|---|---|---|---|---|
| row 0 | 88 | 92 | 95 | 80 |
| row 1 | 75 | 90 | 82 | 99 |
| row 2 | 100 | 84 | 96 | 89 |
与一维数组一样,二维数组元素既可以出现在等号右边(读取),也可以出现在等号左边(赋值)。
7. 一个方括号和两个方括号,得到的不是同一种东西
这是本课最值得真正理解的一点,也是 College Board 的 4.11.A.5。
仍然使用:
int[][] scores = {
{88, 92, 95, 80},
{75, 90, 82, 91},
{100, 84, 96, 89}
};
比较以下三个表达式:
| 表达式 | 得到什么? | 类型 |
|---|---|---|
scores |
整个二维数组 | int[][] |
scores[1] |
第 1 行这个一维数组 | int[] |
scores[1][2] |
第 1 行、第 2 列的一个值 | int |
可以想成:
scores → 整张表
scores[1] → 表中的第二行 {75, 90, 82, 91}
scores[1][2] → 第二行的第三个值 82
7.1 将一整行保存到一个一维数组变量
int[] oneStudent = scores[1];
oneStudent 得到的是第二行数组对象的引用,并没有复制出一份新的行数组。
因此:
oneStudent[2] = 100;
会修改原来第二行中的同一个元素。
现在:
System.out.println(scores[1][2]);
输出:
100
重点: oneStudent 和 scores[1] 仍然引用同一个一维数组。我们修改的是这行数组中的元素,而不是用一个新的一维数组替换它。
8. 行数与列数:为什么有两个 length?
前面一维数组只有一个 length。二维数组因为是数组的数组,所以有外层数组的长度,也有每一行数组的长度。
int[][] scores = {
{88, 92, 95, 80},
{75, 90, 82, 91},
{100, 84, 96, 89}
};
System.out.println(scores.length);
System.out.println(scores[0].length);
输出:
3
4
为什么?
scores.length:外层数组有 3 个元素,也就是有 3 行。scores[0].length:第 0 行是一个一维数组,它有 4 个元素,也就是有 4 列。
8.1 最容易混淆的三个表达式
| 表达式 | 含义 | 本例结果 |
|---|---|---|
scores.length |
行数 | 3 |
scores[0].length |
第 0 行的元素数量,即列数 | 4 |
scores[1].length |
第 1 行的元素数量 | 4 |
在 AP CSA 考试的矩形二维数组中,每行的列数相同,所以 scores[0].length 和 scores[1].length 相等。
不过,从 Java 的语义来说,scores[0].length 仍然是“第 0 行这个一维数组的长度”。
不要误以为 scores.length 是列数,也不要把 scores[0].length 理解成整个二维数组的元素总数。
此外,和一维数组一样,length 是属性,不是 method。正确语法是 .length,不是 .length()。
9. 两个索引都有自己的有效范围(Index Bounds)
对于 new int[3][4]:
- 有 3 行,合法的 row index:
0、1、2。 - 每行有 4 列,合法的 column index:
0、1、2、3。
因此:
| 访问表达式 | 是否有效? | 原因 |
|---|---|---|
scores[0][0] |
有效 | 第一行第一列 |
scores[2][3] |
有效 | 最后一行最后一列 |
scores[3][0] |
无效 | 不存在 row index 3 |
scores[0][4] |
无效 | 不存在 column index 4 |
scores[-1][1] |
无效 | 索引不能为负数 |
越界时会出现:
ArrayIndexOutOfBoundsException
判断任意一个二维数组表达式 arr[r][c] 是否合法,可以分两步:
- 检查行:
0 <= r && r < arr.length - 检查列:
0 <= c && c < arr[r].length
这就是把一维数组的索引规则使用了两次:先检查外层数组,再检查选中的那一行数组。
9.1 三个常见的 off-by-one 错误
int[][] values = new int[2][4];
| 错误理解 | 正确理解 |
|---|---|
| “2 行,所以最后一行是 index 2” | 最后一行是 index 1 |
| “4 列,所以最后一列是 index 4” | 最后一列是 index 3 |
“第三行第二列就是 [3][2]” |
应写成 [2][1](前提是确实存在第三行) |
10. AP Code Tracing:二维数组的代码追踪
遇到代码追踪题,推荐先画出带有 row/column index 的表格,然后每执行一条赋值语句,就只修改对应的一个单元格。
Example 1:从另一个位置取值
int[][] values = {
{2, 5, 8},
{7, 4, 1}
};
values[0][2] = values[1][0] - values[0][1];
values[1][1] = values[0][2] + values[1][2];
System.out.println(values[1][1] + " " + values[0][2]);
初始状态:
| col 0 | col 1 | col 2 | |
|---|---|---|---|
| row 0 | 2 | 5 | 8 |
| row 1 | 7 | 4 | 1 |
第一条赋值语句:
values[0][2] = values[1][0] - values[0][1];
右边先计算:7 - 5 = 2。
所以把 values[0][2] 从 8 改成 2。
第二条赋值语句:
values[1][1] = values[0][2] + values[1][2];
注意:此时 values[0][2] 已经是 2,不再是 8!
所以 values[1][1] = 2 + 1 = 3。
最终状态:
| col 0 | col 1 | col 2 | |
|---|---|---|---|
| row 0 | 2 | 5 | 2 |
| row 1 | 7 | 3 | 1 |
输出:
3 2
这类题的关键不是计算难,而是必须按执行顺序使用最新的数组值。
Example 2:一整行也是数组
int[][] values = {
{10, 20, 30},
{40, 50, 60}
};
int[] row = values[1];
row[0] = values[0][2];
System.out.println(values[1][0]);
答案是:
30
因为 row 指向 values[1] 这一行,row[0] 与 values[1][0] 访问的是同一个元素。
11. Row-Major Order:什么叫“按行”?
Runestone 介绍了两个顺序术语:
- Row-major order(行优先):先处理一整行,再处理下一行。
- Column-major order(列优先):先处理一整列,再处理下一列。
例如:
1 2 3
4 5 6
按行读取的顺序是:
1 → 2 → 3 → 4 → 5 → 6
按列读取的顺序是:
1 → 4 → 2 → 5 → 3 → 6
AP CSA 使用行、列的方式理解二维数组,并要求学生理解其中的“数组的数组”结构。
这里不要把“row-major order”误解成 Java 一定把所有二维数组元素紧挨着存放在一块一维内存里。 在 Java 中,二维数组实际是外层数组引用着各行一维数组。教材介绍行优先和列优先,是为了帮助我们理解访问和处理顺序。
下一课 4.12 2D Array Traversals 才会系统学习利用 nested loops(嵌套循环)遍历整张表。本课先把结构、索引和长度掌握好。
12. AP Exam Focus:六项核心要求
本课在 College Board 2025 版 CED 中的目标是 4.11.A:使用二维数组对象表示相关数据的集合。
| 官方考点 | 学生需要真正掌握什么? |
|---|---|
| 4.11.A.1 | 二维数组是数组的数组;可以保存基本类型值或对象引用;长度创建后固定;考试不涉及非矩形二维数组。 |
| 4.11.A.2 | 使用 new 创建二维数组时,每个元素获得对应类型的默认值。 |
| 4.11.A.3 | 认识并编写嵌套初始化列表,每个内层列表代表一行一维数组。 |
| 4.11.A.4 | 用 array[row][col] 访问或修改一个元素,顺序是行在前、列在后。 |
| 4.11.A.5 | 理解 array[row] 得到的是一整行一维数组,而不是单个元素。 |
| 4.11.A.6 | 用 array.length 获得行数,用 array[row].length 获得这一行的列数;掌握合法索引与越界错误。 |
官方建议技能:
- 2.B:编写涉及数据抽象的代码。
- 3.B:根据涉及数据抽象的代码判断结果或输出。
本课主要训练创建、读取、赋值、判断结果。不要把 4.12 才系统讲解的嵌套循环遍历硬塞进 4.11。
13. Common Mistakes:常见理解错误
| 容易犯的错误 | 正确理解 |
|---|---|
把 new int[3][4] 理解成 4 行 3 列 |
第一组方括号是行数,第二组是列数:3 行 4 列 |
把 array[2][1] 理解成第二行第三列 |
第一个 index 是行,第二个是列;它是第三行第二列 |
认为 array[row] 已经是一个 int |
如果数组类型是 int[][],那么 array[row] 的类型是 int[] |
认为 array.length 是列数 |
array.length 是外层数组长度,也就是行数 |
认为 array[0].length 是所有元素的总数 |
它只是第 0 行的长度(矩形数组中的列数) |
把 .length 写成 .length() |
正确 Java 语法是 .length,不加括号 |
认为 new String[2][3] 自动创建了六个 String 内容 |
六个单元格的初始值都是 null |
| 认为数组的行列索引从 1 开始 | 两个 index 都从 0 开始 |
认为 array[rows][0] 是最后一行第一个元素 |
最后一行是 array[rows - 1] |
认为 array[row] 是复制出的一行新数组 |
它取得的是原有一行数组的引用 |
14. Mini Practice:小练习
请先尝试作答,再看参考答案。
Practice 1:行数、列数与总元素数
int[][] board = new int[4][6];
以下哪一项正确?
A. 6 行,4 列,共 24 个元素
B. 4 行,6 列,共 24 个元素
C. 4 行,6 列,共 10 个元素
D. 6 行,4 列,共 10 个元素
答案:B。 第一组 [4] 表示行数,第二组 [6] 表示每行的列数,总元素数是 4 × 6 = 24。
Practice 2:默认值
boolean[][] flags = new boolean[2][3];
System.out.println(flags[1][2]);
输出是什么?
A. true
B. false
C. null
D. 0
答案:B。 boolean 元素的默认值是 false,索引 [1][2] 也在有效范围内。
Practice 3:读出指定单元格
int[][] nums = {
{3, 6, 9},
{2, 4, 8},
{5, 7, 1}
};
System.out.println(nums[2][1]);
输出是什么?
A. 4
B. 7
C. 8
D. 1
答案:B。 第 2 行(第三行)是 {5, 7, 1},其中 index 1 是 7。
Practice 4:Row Array 与 Element
int[][] values = {
{10, 20, 30},
{40, 50, 60}
};
关于以下代码,哪一项正确?
int[] row = values[1];
int n = values[1][2];
A. row 是 int,n 是 int[]
B. row 是 int[],n 是 int
C. 两个变量都是 int[][]
D. 代码不能编译
答案:B。 values[1] 是整行一维数组;values[1][2] 是这一行中的一个 int,值为 60。
Practice 5:Length 与合法索引
String[][] seats = new String[3][5];
以下哪个表达式会越界?
A. seats[2][4]
B. seats[0][0]
C. seats[1][5]
D. seats[2][0]
答案:C。 合法行索引为 0–2,合法列索引为 0–4,所以 column index 5 越界。
Practice 6:AP 风格代码追踪
int[][] data = {
{2, 5, 8},
{7, 4, 1}
};
data[0][2] = data[1][0] - data[0][1];
data[1][1] = data[0][2] + data[1][2];
System.out.println(data[1][1]);
输出是什么?
A. 3
B. 4
C. 9
D. 10
答案:A。 第一行把 data[0][2] 改成 7 - 5 = 2;第二行使用修改后的新值,得到 2 + 1 = 3。
Practice 7:填写缺失的代码(Code Writing)
已知有一个二维 int 数组 grid,要求:
- 读取最后一行的最后一个元素,保存到
last。 - 把第一行第二列的元素设置为
last。
题目保证 grid 至少有一行、每行至少有两个元素。
请补全:
int last = _______________________;
grid[___][___] = last;
参考答案:
int last = grid[grid.length - 1][grid[0].length - 1];
grid[0][1] = last;
解释:最后一行的 index 是 grid.length - 1,最后一列的 index 是 grid[0].length - 1(考试中的矩形二维数组,每行列数相同);第一行第二列则是 [0][1]。
Practice 8:对象数组(Object References)
Student[][] students = new Student[2][2];
以下哪一项正确?
A. 代码创建四个 Student 对象。
B. 代码创建一个二维数组及其两行一维数组,四个单元格最初都是 null。
C. students[0][0] 自动是一个空姓名的 Student 对象。
D. 二维数组不能保存对象引用。
答案:B。 数组对象已经创建,但 Student 对象没有自动创建;每个引用类型单元格的初始值是 null。
Unit 4.11 核心词汇(Vocabulary)
| Vocabulary | 中文理解 |
|---|---|
| two-dimensional array / 2D array | 二维数组;由多个一维数组组成的数组 |
| row | 行;横向的一排元素 |
| column | 列;纵向的一排元素 |
| array of arrays | 数组的数组;外层数组的元素引用内层数组 |
| outer array | 外层数组;其每个元素保存一行数组的引用 |
| inner array | 内层数组;二维数组中的一行一维数组 |
| rectangular array | 矩形二维数组;每行的元素数量相同 |
| row index | 行索引;array[row][col] 中的第一个 index |
| column index | 列索引;array[row][col] 中的第二个 index |
| initializer list | 初始化列表;用嵌套 {} 提供每一行的初始值 |
| default value | 默认值;使用 new 创建数组时自动设置的初始值 |
| length | 长度;外层数组的长度是行数,行数组的长度是列数 |
| row-major order | 行优先顺序;先处理一行,再处理下一行 |
| ArrayIndexOutOfBoundsException | 数组索引越界异常;访问不存在的行或列时发生 |
下一课预告: 4.11 学会了如何创建二维数组,并准确找到某个单元格。4.12 将继续学习如何使用 nested loops(嵌套循环) 按行或按列遍历整张表格。