Mr.Mou @ ShiShi AP Center

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}

也就是说:

  1. scores 是一个二维数组变量,保存着外层数组对象的引用。
  2. 外层数组有 3 个元素;每个元素保存着一行一维数组的引用。
  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}
};

观察大括号的层次:

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];

思考顺序:

  1. scores[2]:先找到 index 2,也就是第三行 {100, 84, 96, 89}。
  2. [1]:再从这一行里找 index 1,也就是第二个元素。
  3. 得到 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

为什么?

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]:

因此:

访问表达式 是否有效? 原因
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] 是否合法,可以分两步:

  1. 检查行: 0 <= r && r < arr.length
  2. 检查列: 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 介绍了两个顺序术语:

例如:

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 获得这一行的列数;掌握合法索引与越界错误。

官方建议技能:

本课主要训练创建、读取、赋值、判断结果。不要把 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,要求:

  1. 读取最后一行的最后一个元素,保存到 last。
  2. 把第一行第二列的元素设置为 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(嵌套循环) 按行或按列遍历整张表格。