1. 概述
在 Java 中,锯齿数组(Jagged Array)是一种特殊的多维数组,它的每一行可以拥有不同长度的元素。它也被称为“不规则数组”或“数组的数组”——因为它的每个元素本身就是一个数组,且这些数组的大小可以各不相同。
这类结构在处理非对称数据时非常实用,比如表示三角矩阵、动态分组数据等。本文将带你深入理解锯齿数组的声明、初始化方式、内存结构以及常见操作。
✅ 提示:虽然 Java 没有原生支持“多维数组”的概念,但通过数组嵌套数组的方式,实现了灵活的多维结构,锯齿数组正是这种灵活性的体现。
2. 内存结构
在 Java 中,数组本质上是对象,其元素可以是基本类型,也可以是引用类型。因此,一个二维锯齿数组实际上就是一个一维数组,其每个元素都指向另一个一维数组。
来看一张典型的内存示意图:
从图中可以看出:
multiDimensionalArr[0]
指向一个长度为 2 的数组multiDimensionalArr[1]
指向一个长度为 3 的数组multiDimensionalArr[2]
指向一个长度为 4 的数组
⚠️ 注意:由于每一行是独立分配的,所以它们在堆内存中的地址并不连续,这也是“锯齿”名称的由来——形状像参差不齐的边缘。
3. 声明与初始化
Java 提供了多种方式来声明和初始化锯齿数组。下面列举几种常用写法。
3.1. 一步声明并初始化
最简洁的方式是在声明时直接赋值:
int[][] multiDimensionalArray = {{1, 2}, {3, 4, 5}, {6, 7, 8, 9}};
✅ 这种写法适用于已知所有数据的场景,三行分别有 2、3、4 个元素,完美体现“不规则”。
3.2. 仅声明,不初始化元素
你可以先声明数组结构,延迟初始化各行:
int[][] multiDimensionalArray = new int[3][];
此时,multiDimensionalArray
是一个长度为 3 的数组,但每个元素都是 null
(因为是引用类型数组),需要后续手动填充。
❌ 踩坑提醒:此时不能直接访问
multiDimensionalArray[0][0]
,会抛出NullPointerException
。
3.3. 声明后逐行初始化
接着上一步,我们可以为每一行分配具体数组:
multiDimensionalArray[0] = new int[] {1, 2};
multiDimensionalArray[1] = new int[] {3, 4, 5};
multiDimensionalArray[2] = new int[] {6, 7, 8, 9};
这种方式适合动态构建场景,比如从文件或数据库读取不同长度的记录。
3.4. 指定每行大小后再填充
也可以先指定每行容量,再填值:
multiDimensionalArray[0] = new int[2];
multiDimensionalArray[1] = new int[3];
multiDimensionalArray[2] = new int[4];
然后逐个赋值:
multiDimensionalArray[0][0] = 1;
multiDimensionalArray[0][1] = 2;
multiDimensionalArray[1][0] = 3;
multiDimensionalArray[1][1] = 4;
multiDimensionalArray[1][2] = 5;
multiDimensionalArray[2][0] = 6;
multiDimensionalArray[2][1] = 7;
multiDimensionalArray[2][2] = 8;
multiDimensionalArray[2][3] = 9;
⚠️ 优点是能提前控制内存分配;缺点是代码啰嗦,仅在需要精确控制时使用。
3.5. 其他初始化方式
还有一种更显式的写法,适合复杂初始化逻辑:
int[][] multiDimensionalArray = new int[][] {
new int[] { 1, 2 },
new int[] { 3, 4, 5 },
new int[] { 6, 7, 8, 9 }
};
或者更简洁地省略外层类型声明:
int[][] multiDimensionalArray = {
new int[] { 1, 2 },
new int[] { 3, 4, 5 },
new int[] { 6, 7, 8, 9 }
};
✅ 推荐使用最后这种写法,清晰且符合 Java 语法习惯。
4. 打印锯齿数组元素
要打印锯齿数组的内容,常用两种方式。
使用嵌套 for 循环
for (int i = 0; i < multiDimensionalArray.length; i++) {
for (int j = 0; j < multiDimensionalArray[i].length; j++) {
System.out.print(multiDimensionalArray[i][j] + " ");
}
System.out.println();
}
输出结果:
1 2
3 4 5
6 7 8 9
✅ 简单粗暴,适合调试或格式化输出。
使用 Arrays.toString()
更简洁的方式是利用工具类:
for (int index = 0; index < multiDimensionalArray.length; index++) {
System.out.println(Arrays.toString(multiDimensionalArray[index]));
}
输出:
[1, 2]
[3, 4, 5]
[6, 7, 8, 9]
✅ 推荐用于日志打印或快速查看内容,避免手写内层循环。
5. 获取每行长度
由于每行长度不同,有时我们需要提取各行的长度信息。
传统方式:遍历收集
int[] findLengthOfElements(int[][] multiDimensionalArray) {
int[] arrayOfLengths = new int[multiDimensionalArray.length];
for (int i = 0; i < multiDimensionalArray.length; i++) {
arrayOfLengths[i] = multiDimensionalArray[i].length;
}
return arrayOfLengths;
}
调用示例:
int[] lengths = findLengthOfElements(multiDimensionalArray);
// 结果: [2, 3, 4]
函数式方式:使用 Stream
更现代的写法:
Integer[] findLengthOfArrays(int[][] multiDimensionalArray) {
return Arrays.stream(multiDimensionalArray)
.map(array -> array.length)
.toArray(Integer[]::new);
}
⚠️ 注意返回类型是 Integer[]
而非 int[]
,因为 toArray()
需要泛型构造器引用。
✅ Stream 写法更简洁,适合函数式编程风格项目。
6. 深拷贝二维数组
锯齿数组的拷贝必须是深拷贝,否则会出现引用共享问题。
手动深拷贝
int[][] copy2DArray(int[][] arrayOfArrays) {
int[][] copied2DArray = new int[arrayOfArrays.length][];
for (int i = 0; i < arrayOfArrays.length; i++) {
int[] array = arrayOfArrays[i];
copied2DArray[i] = Arrays.copyOf(array, array.length);
}
return copied2DArray;
}
✅ 核心是每一行都调用 Arrays.copyOf()
,确保底层数据独立。
使用 Stream 实现深拷贝
Integer[][] copy2DArray(Integer[][] arrayOfArrays) {
return Arrays.stream(arrayOfArrays)
.map(array -> Arrays.copyOf(array, array.length))
.toArray(Integer[][]::new);
}
⚠️ 注意:这里使用的是 Integer[][]
,因为泛型不支持基本类型。若需 int[][]
,仍建议用传统方式。
✅ 函数式风格,代码紧凑,适合函数式优先的代码库。
7. 总结
锯齿数组作为 Java 多维数组的一种灵活实现,适用于数据长度不一的场景。关键点总结如下:
- ✅ 锯齿数组 = 数组的数组,每行长度可变
- ✅ 内存中各行独立分配,非连续
- ✅ 声明时可不指定列数,如
new int[3][]
- ✅ 初始化方式多样,推荐使用
{ { }, { }, { } }
语法 - ✅ 遍历时注意空行或未初始化行,避免 NPE
- ✅ 拷贝必须深拷贝,逐行复制
掌握锯齿数组,能让你在处理不规则数据结构时更加得心应手。虽然日常开发中使用频率不高,但在算法题、矩阵运算或配置解析中,往往是关键利器。