跳转至

多维数组

你将习得:核心概念,声明语法,初始化方式,长度获取,元素操作,遍历技巧,内存结构,不规则数组,常见异常

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,没有指向任何内层数组对象