1. 概述

在 Java 中,锯齿数组(Jagged Array)是一种特殊的多维数组,它的每一行可以拥有不同长度的元素。它也被称为“不规则数组”或“数组的数组”——因为它的每个元素本身就是一个数组,且这些数组的大小可以各不相同。

这类结构在处理非对称数据时非常实用,比如表示三角矩阵、动态分组数据等。本文将带你深入理解锯齿数组的声明、初始化方式、内存结构以及常见操作。

✅ 提示:虽然 Java 没有原生支持“多维数组”的概念,但通过数组嵌套数组的方式,实现了灵活的多维结构,锯齿数组正是这种灵活性的体现。


2. 内存结构

在 Java 中,数组本质上是对象,其元素可以是基本类型,也可以是引用类型。因此,一个二维锯齿数组实际上就是一个一维数组,其每个元素都指向另一个一维数组

来看一张典型的内存示意图:

memory representation of jagged array

从图中可以看出:

  • 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
  • ✅ 拷贝必须深拷贝,逐行复制

掌握锯齿数组,能让你在处理不规则数据结构时更加得心应手。虽然日常开发中使用频率不高,但在算法题、矩阵运算或配置解析中,往往是关键利器。


原始标题:Jagged Arrays in Java | Baeldung