☰
Java二维数组从内存模型到Arrays工具类:初始化、遍历与实战避坑
2026/10/24 2:56:26 网站建设 项目流程

学习笔记更新到 1.11 了。前面几篇把变量、运算符、流程控制都过了一遍,本来以为数组这一节能轻松翻过去,结果发现一维数组只是开了个头,后面跟着的多维数组、Arrays 工具类、各种边界条件,才是真正考验耐心的部分。这篇笔记专门记录我学二维数组和数组实操时整理的内容,包括内存模型、初始化方式、遍历套路、几个经典小案例,以及我没少踩的坑。适合刚学完 Java 基础语法、准备系统过一遍数组知识的人参考,如果你已经会用一维数组做冒泡排序,那这篇正好接着往后看。

1. 为什么数组要单独花一篇笔记来讲

很多初学者觉得数组不就是"存一堆数据"吗,学个循环就能用了。这种想法在练习题里勉强够用,等真正写起代码来会发现,数组涉及的知识密度远超预期。它一方面是一维、二维、多维的存法问题,另一方面是内存中引用如何指向的问题,后面学集合框架、IO、对象数组时,全部建立在理解数组的基础上。

1.1 一维数组只是入口

一维数组解决的是"同一类型的多个数据怎么存"这个基本问题。比如存班级里的 50 个成绩,用int[] scores = new int[50]就搞定了。真正让人晕的是二维数组,因为二维数组在逻辑上是表格,在内存里却是"数组的数组",这个概念如果没揉碎理解了,后面写矩阵相关算法、处理表格数据、做图像像素遍历时,会一直踩空。

我在学习时给自己定的目标是:不靠死背语法,而是能在纸上画出某个new int[3][4]在堆内存里长什么样。画清楚了,所有下标问题、越界问题、空指针问题,都能从图上直接看出来。

1.2 这篇笔记的练习方式

由于是学习笔记,我尽量把每个知识点配一段能直接运行的示例代码。我的建议也一样:看一段,敲一段,改一改参数再跑一次。Java 的语法细节光靠看是记不住的,特别是数组下标这种"差一个就崩"的东西,只有亲手把ArrayIndexOutOfBoundsException摔过一次,记忆才深刻。

2. 二维数组的内存模型:一张表格在内存里的真实样子

我第一次学二维数组时,老师给的说法是"二维数组就是表格"。这句话对理解逻辑有帮助,但对排查问题没帮助。因为int[][] arr = new int[3][4]并不是在内存里画了一张真正的二维表,而是一组嵌套的引用。

2.1 逻辑上像表格,物理上却是"数组的数组"

可以用书架来类比:arr本身是一个书架,它有 3 层,每层是一个一维数组;每一层里的 4 个格子,才是真正存放int数值的位置。也就是说,arr[0]指向第一个一维数组,arr[0][2]才最终定位到某个数值。

int[][] arr = new int[3][4]; System.out.println(arr.length); // 3,书架有3层 System.out.println(arr[0].length); // 4,第一层有4个格子

这个区分特别容易出错。很多人写循环遍历二维数组时,外层用arr.length,内层也顺手用arr.length,结果列数多时就漏数据,列数少时就越界。正确写法是内层用arr[i].length,因为每一行的一维数组长度才是真正的列数。

2.2 用代码验证内存结构

如果你打印arr和arr[0],会看到类似[[I@1b6d3586、[I@4554617c这样的输出。方括号个数代表维度,[[I表示 int 类型的二维数组,[I表示 int 类型的一维数组。这串字符实际上是数组对象在堆内存中的地址标识,它直接证明了:

  • arr是一个引用变量,存在栈上,指向堆里的某个数组对象;
  • 这个数组对象的每个元素,又是一个引用,指向另外的数组对象;
  • 只有最内层的数组元素,才真正存着数值。

理解这一点后,很多问题都通了。比如"为什么二维数组可以是不规则的?"答案就在内存模型里:每一层的一维数组是独立创建的,长度完全可以不同,Java 没有规定它们必须一样长。

3. 三种初始化方式与遍历的细节

3.1 静态初始化:适合数据已知的场景

如果一开始就知道数据,用静态初始化最省事,也最直观:

int[][] matrix = { {1, 2, 3}, {4, 5, 6}, {7, 8, 9} };

这种写法的好处是代码和表格结构一一对应,一眼就能看出哪行哪列是什么值。我在做矩阵相关练习题时基本都用这种,调试时也方便,因为每行数据都在一行代码里。

3.2 动态初始化与不规则数组

数据未知、需要运行时填充时,用动态初始化。这里要注意一个细节:new int[3][4]是一次性把三行四列全建好,而new int[3][]只创建了三个"空行引用",每一行的一维数组要单独创建:

int[][] arr = new int[3][]; arr[0] = new int[2]; arr[1] = new int[5]; arr[2] = new int[3];

这就是不规则数组,也叫锯齿数组。它有什么实际用处?比如统计某商场各楼层店铺的月销售额,一层有 8 家店、二层有 12 家、三层只有 4 家,你完全没必要硬凑成一个 3 行 12 列的规则矩形,用锯齿数组更贴合真实数据。

3.3 普通 for 循环和增强 for 循环的取舍

遍历二维数组时,两种循环都能用,但能力不等价。

// 增强 for:只读 for (int[] row : matrix) { for (int value : row) { System.out.print(value + " "); } System.out.println(); } // 普通 for:可读写 for (int i = 0; i < matrix.length; i++) { for (int j = 0; j < matrix[i].length; j++) { matrix[i][j] *= 2; } }

增强 for 的局限在于,它把元素取出来赋给一个新的局部变量,你修改value不会影响原数组。需要按下标定位并修改元素时,必须回到普通 for。这个细节我在初学时吃过亏,以为增强 for 是"万能简化版",结果某次想统一给矩阵加 1,循环跑完打印一看,数组纹丝不动。

4. 三个实战小案例:矩阵操作

学数组不练矩阵就说不过去,矩阵是最典型的二维数组场景。我把几个经典练习记录在这里,每个都带完整代码和踩坑点。

4.1 矩阵转置:行列互换

给定一个矩阵,求它的转置,即把matrix[i][j]放到新矩阵的result[j][i]。

public static int[][] transpose(int[][] matrix) { int rows = matrix.length; int cols = matrix[0].length; int[][] result = new int[cols][rows]; for (int i = 0; i < rows; i++) { for (int j = 0; j < cols; j++) { result[j][i] = matrix[i][j]; } } return result; }

这里的坑在于新矩阵的行列数是反过来的。原矩阵 3 行 2 列,转置后就是 2 行 3 列。如果你写成new int[rows][cols],赋值时result[j][i]一定会越界。我建议在纸上把原本的坐标写出来,再画一个转置后的框,逐个对应一遍再写代码。

4.2 行列求和与对角线求和

方阵的对角线求和是很常见的题目,但里面有两个容易忽略的细节:主对角线是i == j的位置,副对角线是i + j == n - 1的位置;当 n 为奇数时,正中间的元素会被两条对角线同时算到,需要判断是否重复累加。

public static int diagonalSum(int[][] matrix) { int n = matrix.length; int sum = 0; for (int i = 0; i < n; i++) { sum += matrix[i][i]; // 主对角线 sum += matrix[i][n - 1 - i]; // 副对角线 } if (n % 2 == 1) { sum -= matrix[n / 2][n / 2]; // 去重 } return sum; }

这个去重逻辑是题目的灵魂。你不写那行减法,奇数阶矩阵的结果就会比正确答案多一个中心元素。我第一次写就是漏了这一步,输出的结果怎么验都不对,最后 debug 打印每次累加的值才发现问题。

4.3 常见边界错误

做矩阵题最容易碰到的问题,我总结下来就是三个:

  • 行列概念混淆。外层循环管行,内层循环管列,下标别写反。
  • 方阵假设放之四海而皆准。不是所有矩阵都是方阵,matrix.length和matrix[0].length代表不同的东西。
  • 空矩阵或空行。方法入口处最好判断一下matrix == null || matrix.length == 0,否则一进来就取matrix[0]直接空指针。

5. Arrays 工具类:排序、查找和拷贝的正确打开方式

学完数组的基本操作后,我开始接触java.util.Arrays。这个工具类提供了一系列静态方法,能极大简化日常操作,但每个方法都有隐藏前提,用错的人不在少数。

5.1 toString、sort、binarySearch

直接打印数组名得到的是地址,所以有了Arrays.toString()来查看元素内容。一维数组用toString,多维数组要用deepToString,否则外层打印的还是地址:

int[][] matrix = {{1, 2}, {3, 4}}; System.out.println(Arrays.toString(matrix)); // [[I@xxx, [I@xxx] System.out.println(Arrays.deepToString(matrix)); // [[1, 2], [3, 4]]

sort默认升序排列,排序是原地操作,会直接修改原数组。而binarySearch有两个硬性前提:数组必须已排序,且元素必须可比较。很多人排序完忘了binarySearch只能在升序数组上工作,或者拿一个未排序数组直接搜,结果要么负值要么完全错误。注意:如果数组中存在重复元素,binarySearch不保证返回哪一个下标。

5.2 copyOf 与数组扩容

数组长度在创建后就固定了,所谓"扩容"其实是创建新数组并把旧数据拷过去。Arrays.copyOf就是这个操作的标准工具:

int[] arr = {1, 2, 3}; arr = Arrays.copyOf(arr, 5); // 扩容到5,多余位置补0

底层调用了System.arraycopy这个 native 方法,性能很好。在模拟实现 ArrayList 自动扩容时,这套逻辑就是原型:判断容量不够就扩大到原来的 1.5 倍,然后copyOf搬家。

5.3 深浅拷贝:二维数组拷贝的坑

copyOf对一维数组是深拷贝,对二维数组却只是浅拷贝。因为二维数组的元素是引用,copyOf(matrix, matrix.length)拷过来的是那一行一维数组的引用地址,并没有真正复制每行的元素。修改拷贝结果里的某个值,原数组也可能被改动。

int[][] matrix = {{1, 2}, {3, 4}}; int[][] copy = Arrays.copyOf(matrix, matrix.length); copy[0][0] = 99; System.out.println(matrix[0][0]); // 99,原数组被改了

要真正复制一个二维数组,需要逐层拷贝,或者用循环手动复制每个一维数组。这是一个非常经典、面试中也常被问到的话题,建议自己动手写一遍逐行拷贝的版本。

6. 学习笔记里的避坑清单

6.1 下标越界为什么总在循环里出现

ArrayIndexOutOfBoundsException是数组学习中出现频率最高的异常。我观察自己的代码,发现绝大多数越界都发生在一个场景:内层循环条件写错。比如把j < matrix[i].length写成j < matrix.length,当列数大于行数时,j就会超过某一行的实际长度。另一个常见错误是遍历不规则数组时,不取当前行的长度,而是取了第一行的长度。

我的习惯是:凡是用到数组长度的地方,先在注释里写下"这是行数还是列数"。写清楚后真的能避免大量低级问题。

6.2 空指针往往来自"你以为数组不为空"

二维数组的动态初始化有个陷阱:new int[3][]创建后,三个位置都是null,还没指向任何一维数组。如果你直接赋值,比如arr[0][0] = 5,就会抛NullPointerException。必须先arr[0] = new int[4]再赋值。

这个坑在读取外部数据时尤其常见。比如从配置文件或接口拿到一个可能为空的数组,不加判断就直接遍历,轻则报错,重则在业务代码里引发连锁异常。防御性检查放在方法入口,是成本最低的做法。

6.3 增强 for 的三个限制

再次强调增强 for 的局限,因为它太容易让人误用了:

  • 不能获取当前下标,需要下标时只能自己加一个计数器(那还不如用普通 for);
  • 不能修改原数组元素,修改的是局部变量副本;
  • 遍历过程中不能安全地删除或修改集合结构,这在数组里表现不明,但在后面学集合时会放大。

6.4 调试数组的两个小技巧

分享两个我在练习中觉得非常好用的技巧。一个是打印中间状态:循环里每隔几步就把当前数组用Arrays.deepToString打印一次,肉眼观察数据变化,比断点更直觉。另一个是"最小复现法":出问题时把数组规模缩小,比如 3 行 3 列变成 2 行 2 列,手动在纸上演算一遍,往往一眼就看出逻辑错在哪。

我整理这份笔记时特意把每个知识点都配了可以独立运行的代码段,并标注了对应的输出或报错结果,目的就是让以后翻笔记的自己能快速回忆起当时的思路。学数组的阶段会有一点枯燥,因为它不像做界面、写项目那样立刻有成就感,但它是后续所有数据结构的地基。我个人的体会是,认真把二维数组的内存模型和三种初始化方式吃透,后面学 ArrayList、HashMap 时的理解成本会低很多。如果这份笔记能帮到同样在啃 Java 基础的人,那就再好不过了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询