多维数组
你将习得:核心概念,声明语法,初始化方式,长度获取,元素操作,遍历技巧,内存结构,不规则数组,常见异常
1. 核心概念:数组的数组¶
在 Java 中,其实没有真正意义上的“多维数组”。所谓的多维数组,本质上是**“数组的数组”**。
- 一个一维数组的每个元素如果又是一个一维数组,那么它就成了二维数组。
- 这种特性使得 Java 中的多维数组非常灵活,因为内层数组的长度可以不一致。
2. 声明语法¶
声明二维数组时,推荐将方括号放在类型后面,这样可读性最强。
// ✅ 推荐写法:清晰表明这是一个二维的 int 数组
int[][] arr;
// 允许但不推荐的写法(C语言风格):
int arr[][];
int[] arr[];
错误:int[][] arr1, arr2[][], []arr3[];
编译器会提取出**公共的基本类型**,然后依次解析后面的变量:
- 公共基本类型:
int[][] - 第一个变量:
arr1 - 合法。
arr1的最终类型是int[][]。 - 第二个变量:
arr2[][] - 合法。
arr2会在公共基础(二维)上再加二维,变成**四维数组** (int[][][][])。 - 第三个变量:
[]arr3[] - 语法错误! 编译器在逗号之后期待看到的是一个变量名(比如
arr3),但却先看到了一个中括号[]。在 Java 的逗号分隔声明列表中,[]是不允许出现在变量名左侧的。
3. 初始化方式¶
多维数组的初始化分为静态和动态两种,必须在声明时或声明后为其分配内存。
静态初始化(已知具体元素)
// 声明的同时赋值,由编译器推断大小
int[][] arr1 = { {1, 2}, {3, 4, 5}, {6} };
// 完整写法
int[][] arr2 = new int[][]{ {1, 2}, {3, 4}, {5, 6} };
动态初始化(已知大小,后续赋值)
// 规则数组:创建一个 3行 4列 的二维数组,初始值默认为 0
int[][] arr3 = new int[3][4];
// 不规则数组(只指定外层大小):创建一个长度为 3 的二维数组,内层数组暂不分配
int[][] arr4 = new int[3][];
4. 长度获取¶
要想遍历或操作多维数组,必须清楚如何获取各个维度的长度。
int[][] arr = { {1, 2}, {3, 4, 5} };
// 获取外层数组的长度(即“行数”)
int rows = arr.length; // 结果为 2
// 获取某个内层数组的长度(即某一行的“列数”)
int colsOfFirstRow = arr[0].length; // 结果为 2
int colsOfSecondRow = arr[1].length; // 结果为 3
5. 元素操作¶
通过多重索引(从 0 开始)来定位具体位置的元素。
int[][] arr = new int[2][3];
// 赋值(修改)
arr[0][1] = 10; // 将第一行第二列的元素设为 10
// 访问(读取)
int value = arr[0][1];
6. 遍历技巧¶
遍历二维数组通常需要两层嵌套循环。外层控制行,内层控制列。
常规 for 循环(需要使用索引时推荐)
int[][] arr = { {1, 2}, {3, 4} };
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr[i].length; j++) {
System.out.print(arr[i][j] + " ");
}
System.out.println(); // 换行
}
增强型 for 循环(只需读取元素时推荐,代码更简洁)
for (int[] row : arr) { // 外层取出来的是一维数组
for (int element : row) { // 内层取出来的是具体元素
System.out.print(element + " ");
}
System.out.println();
}
7. 内存结构¶
理解内存是真正掌握多维数组的关键。
- 栈内存 (Stack):保存变量名
arr,它存储的是外层数组在堆中的地址。 - 堆内存 (Heap):
- 外层数组存放的是**引用(地址)**,指向各个内层数组。
- 内层数组才是真正连续存放**具体数据**的地方。
案例:
int[][] arr = new int[2][]; // 第一步 & 第二步
arr[0] = new int[3]; // 第三步
// 注意:arr[1] 此时还没有被初始化
graph LR
Stack_arr[栈内存: arr 变量] -->|存储外层地址| Heap_Outer[堆内存: 外层数组]
Heap_Outer -->|arr 0 存储内层地址| Heap_Inner1[堆内存: 内层一维数组]
Heap_Outer -->|arr 1 尚未分配| Null_Value[null]
Heap_Inner1 -->|索引 0| Element_0[值: 0]
Heap_Inner1 -->|索引 1| Element_1[值: 0]
Heap_Inner1 -->|索引 2| Element_2[值: 0]
- 第一步:栈内存声明 (
int[][] arr) 程序在 栈内存 (Stack) 中为变量arr开辟了一小块空间。此时它还是个空壳,等待接收一个地址。 - 第二步:开辟外层数组 (
new int[2][]) 程序在 堆内存 (Heap) 中划出一块连续空间,长度为 2。 重点:因为它是二维数组的外层,所以这 2 个格子里 只能装内存地址,不能装具体的数字。由于还没赋值,这两个格子的默认值都是null。随后,这块空间的内存地址被交给了栈中的arr。 - 第三步:开辟内层数组 (
arr[0] = new int[3]) 程序再次在 堆内存 的另一个随机位置,划出一块长度为 3 的连续空间。 重点:这是一个普通的一维int数组,所以这 3 个格子里装的是 具体的整数(默认初始化为0)。接着,程序把这个新数组的内存地址,填入到外层数组的第一个格子arr[0]中。
为什么会报空指针异常?
如果此时你直接调用 arr[1][0] = 5;,程序会先去找 arr[1]。你看图就知道,arr[1] 指向的是 null(它没有自己的内层数组),你对一个 null 强行索要第 0 个元素,自然就触发了 NullPointerException。
8. 不规则数组(锯齿数组)¶
因为外层数组存的只是地址,所以内层数组的长度完全可以不一样。这在处理非对称数据时非常节省内存。
int[][] jaggedArr = new int[3][]; // 先只分配外层长度
jaggedArr[0] = new int[2]; // 第一行 2 个元素
jaggedArr[1] = new int[4]; // 第二行 4 个元素
jaggedArr[2] = new int[1]; // 第三行 1 个元素
jaggedArr[1][3] = 99; // 正常赋值
9. 常见异常¶
操作多维数组时,最容易踩的两个坑:
ArrayIndexOutOfBoundsException(数组越界异常):
索引小于 0 或者大于等于该维度的 length。
错误示例:arr[0][4] 当 arr[0] 只有 3 个元素时。
NullPointerException(空指针异常):
在只动态初始化了外层数组(未初始化内层数组)时,直接访问内层元素。
错误示例:
int[][] arr = new int[3][];
arr[0][0] = 5; // 报错!因为 arr[0] 此时还是 null,没有指向任何内层数组对象