1. 二维数组到底是什么——先建立表格思维说真的我一开始学 Java 的时候二维数组这节直接把我整懵了。一维数组好理解一排格子嘛二维数组你说它是表格也行是坐标系也行但写着写着就发现它跟我想的完全不是一回事。趁着复习 Java 基础我把二维数组从头到尾又过了一遍踩过的坑、绕过的弯都记在小本本上干脆整理成一篇日记式的复习笔记。这篇东西适合正在复习 Java 基础、准备面试或者刚开始学数组的人看尤其是那种好像会了、一写就错的状态应该能帮你省不少时间。1.1 从一维到二维生活化类比一维数组可以理解成一排储物柜下标就是柜子编号从 0 开始数。二维数组呢最简单的理解就是一张 Excel 表格有行有列。你要定位一个格子得说清楚第几行第几列对应到代码里就是arr[行][列]。打个比方一维数组是超市的货架一个编号找一件商品二维数组是电影院座位必须知道第几排第几号才能找到座位。Java 里写int[][] seats new int[10][20];意思就是搞一个 10 排、每排 20 个座位的放映厅总共能坐 200 个人。但这只是表面理解。真正让很多人包括我自己栽跟头的是二维数组在 Java 里的底层结构。如果你只是把它当成表格那后面学内存模型、学不规则数组、学拷贝的时候就会觉得处处别扭。所以我的建议是先有一个直觉上的表格模型然后尽快升级到数组的数组这个准确模型。1.2 Java 二维数组的内存真相Java 里的二维数组官方叫法是数组的数组array of arrays。什么意思就是外层是一个一维数组这个数组里存的不是数据本身而是内层数组的引用。我复习的时候把内存图画了一遍感觉一下就通了。比如int[][] arr new int[2][3];它干了三件事在堆内存里创建一个长度为 2 的外层数组类型是int[]里面存的是引用创建两个长度为 3 的内层数组类型是int里面存的是实际的整数把两个内层数组的地址分别放进外层数组的arr[0]和arr[1]。用大白话说外层数组像一个通讯录里面存的不是人是每个人的电话号码真正的数据在那个人身上也就是内层数组里。所以arr.length是外层长度等于行数arr[0].length是第一行的长度等于列数。我当时在这儿反复确认过一件事arr[1]这个东西到底是什么类型答案是int[]它本身又是一个一维数组。这就解释了为什么可以用for (int[] row : arr)这种遍历方式——因为 for-each 取出的是一个个一维数组然后再对着这个一维数组做一次遍历。听课的时候觉得数组的数组这个说法挺抽象但自己把内存图画一遍之后就再也忘不掉了。建议你复习的时候也动手画一画这比看十遍文档都管用。2. 声明、初始化与基础遍历2.1 三种创建方式怎么选Java 里创建二维数组主要有三种写法我看着简单实际上每种都有讲究。先上代码// 方式一指定行数和列数元素使用默认值 int[][] arr1 new int[3][4]; // 方式二只指定行数列数后面再分配 int[][] arr2 new int[3][]; // 方式三静态初始化创建时直接赋值 int[][] arr3 { {1, 2, 3}, {4, 5, 6}, {7, 8, 9} };方式一最常用适合你已经知道矩阵规模、后面再往里面填数据的场景。注意new int[3][4]之后所有元素都是默认值 0不会报错。方式二看起来挺奇怪new int[3][]只确定有几行但每一行还没分配。这种写法是给不规则数组准备的后面会详细说。但这里有个特别重要的坑如果你用方式二创建了arr2在给每一行分配空间之前arr2[0]的值是null。你要是这时候直接写arr2[0][0] 1等着你的就是NullPointerException。我复习第一天就踩了这个代码编译能过一跑就崩找了半天才反应过来。方式三是静态初始化写代码的时候就知道具体数据用花括号直接列出来。这种方式最直观也最适合练习和写测试用例。注意每一行用一对花括号包起来行与行之间用逗号隔开。三种方式我给你的选择建议是数据已知用静态初始化数据规模和规则都明确、后面要动态填值用方式一每一行长度不固定、需要灵活分配用方式二。别小看这个选择写出的代码可读性和健壮性差很多。2.2 不规则数组每一行长度可以不一样C 语言里二维数组是铁板一块每一行必须一样长。Java 因为底层是数组的数组所以每一行可以单独指定长度这就是不规则数组也叫交错数组Jagged Array。最经典的例子就是杨辉三角每一行元素个数比行号多 1int[][] yangHui new int[5][]; for (int i 0; i yangHui.length; i) { yangHui[i] new int[i 1]; yangHui[i][0] 1; yangHui[i][i] 1; for (int j 1; j i; j) { yangHui[i][j] yangHui[i - 1][j - 1] yangHui[i - 1][j]; } }这段代码里new int[5][]创建的就是一个只能放 5 个引用的一维数组每个引用先指向null。循环里每次new int[i 1]给当前这一行分配不同长度的空间。跑完之后第一行长度 1第二行长度 2以此类推。处理不规则数组最需要注意的就是别默认每行长度一样。你用arr[0].length当列数遍历如果行长度不一就会越界。正确做法是内层循环以arr[i].length为准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(); }我复习的时候特意把行循环用arr.length、列循环用arr[i].length这句话写在了本子封面上。虽然听起来像废话但你看网上多少报ArrayIndexOutOfBoundsException的新手帖都是在这儿翻车的。2.3 遍历二维数组的几种姿势遍历是二维数组最基础的操作但写法也有讲究。最标准的是普通嵌套 for 循环上面已经写了。另一种是 for-eachfor (int[] row : arr) { for (int value : row) { System.out.print(value ); } System.out.println(); }外层取出来的是一个int[]行数组内层再遍历这个行数组。这种写法简洁没有下标不容易越界。但代价是你拿不到当前是第几行第几列。如果你需要在遍历的同时知道位置或者要修改某个元素老老实实用带下标的普通 for。还有一个小众但很方便的打印技巧用Arrays工具类System.out.println(Arrays.toString(arr1)); // 打印一维数组 System.out.println(Arrays.deepToString(arr3)); // 打印二维及以上数组deepToString是专门处理数组里套数组的。我在复习的时候经常用它快速看一个二维数组的内容省得自己写双重循环。不过要注意Arrays.equals对二维数组的深比较要用deepEquals普通的equals比较的是外层数组的引用两个内容相同的二维数组用equals比较会返回 false。这个细节我放在后面的坑里细说。3. 典型操作实战从求和到矩阵乘法3.1 求和、最大值与对角线复习二维数组光会创建和遍历是不够的得拿几个经典操作练手。第一个是求和和找最大值这是所有矩阵类题目的基本功int[][] data { {3, 1, 4}, {1, 5, 9}, {2, 6, 5} }; int sum 0; int max Integer.MIN_VALUE; for (int[] row : data) { for (int value : row) { sum value; if (value max) { max value; } } }这里的 min 初始化用Integer.MIN_VALUE是个好习惯。你要是初始化为 0万一数组里全是负数最大值就会算成 0那就有意思了。再进阶一点算主对角线从左上到右下和副对角线从右上到左下的和。对角线操作高度依赖下标必须用普通 forint mainDiagonal 0; int antiDiagonal 0; int n data.length; for (int i 0; i n; i) { mainDiagonal data[i][i]; antiDiagonal data[i][n - 1 - i]; }主对角线好理解行和列下标相等。副对角线是行加列等于n - 1所以列下标是n - 1 - i。这个细节我一开始总写反后来干脆把公式抄在代码注释里。只有方阵才有对角线一说做题前一定要判断一下是不是 n x n 的矩阵。3.2 矩阵转置转置就是把矩阵的行变成列。比如原来的第 0 行变成转置后的第 0 列。代码不复杂int[][] matrix { {1, 2, 3}, {4, 5, 6} }; int rows matrix.length; int cols matrix[0].length; int[][] transposed new int[cols][rows]; for (int i 0; i rows; i) { for (int j 0; j cols; j) { transposed[j][i] matrix[i][j]; } }注意几个点原矩阵是 2 行 3 列转置后是 3 行 2 列所以新数组的行数是原矩阵的列数新数组的列数是原矩阵的行数。赋值的时候下标是反着来的transposed[j][i]不是transposed[i][j]这是新手最容易写错的地方。如果原矩阵是方阵可以在原地转置不用开新数组方法是只遍历右上角区域然后交换for (int i 0; i n; i) { for (int j i 1; j n; j) { int temp matrix[i][j]; matrix[i][j] matrix[j][i]; matrix[j][i] temp; } }这里内层循环从i 1开始是关键。如果从 0 开始每个元素会被交换两次等于白干。我当时还犯过对角线上的元素也跟着交换的毛病虽然对角线交换自己等于没变但逻辑上属于多余操作写代码时尽量保持清晰。3.3 矩阵乘法矩阵乘法是二维数组操作里比较综合的一道题也是面试的高频手写题。规则一句话A 是 m x nB 是 n x p乘积 C 是 m x p其中C[i][j] A[i][0]*B[0][j] A[i][1]*B[1][j] ... A[i][n-1]*B[n-1][j]。大白话就是C 的第 i 行第 j 列等于 A 的第 i 行和 B 的第 j 列做点积。代码如下int[][] a { {1, 2}, {3, 4} }; int[][] b { {5, 6}, {7, 8} }; int m a.length; // A 的行数 int n a[0].length; // A 的列数也是 B 的行数 int p b[0].length; // B 的列数 int[][] c new int[m][p]; for (int i 0; i m; i) { for (int j 0; j p; j) { for (int k 0; k n; k) { c[i][j] a[i][k] * b[k][j]; } } }三层循环里最内层的 k 是在做A 的行与B 的列的对应相乘累加。这个 k 的作用很多人一开始想不通我后来用笔在纸上把两个矩阵画出来手写算了一遍 C[0][0]才真正明白它是在拿 A 的第 0 行挨个和 B 的第 0 列对齐相乘。建议你复习的时候也拿 2x2 的小矩阵手算一遍比看十遍代码都有用。还有一点容易漏矩阵乘法要求 A 的列数必须等于 B 的行数否则无法相乘。代码里要先做这个判断返回 null 或者抛异常不然循环里b[k][j]就会越界。4. 新手必看二维数组的几个经典坑4.1 越界与空指针二维数组最常见的两个异常一个是ArrayIndexOutOfBoundsException一个是NullPointerException我复习期间俩都踩过。越界多是行列不分造成的。比如一个 3 行 4 列的数组你写arr[4][3]行下标最大是 2直接越界。还有个隐蔽场景是使用不规则数组时有人习惯用第一行的长度去遍历所有行结果第二行短一点就炸了。所以遍历不规则数组内层循环的边界一定要写arr[i].length不要图省事用arr[0].length。空指针则多和只创建外层数组、没创建内层数组有关。int[][] arr new int[3][];之后arr[0]是 null任何arr[0][...]操作都会抛空指针。我当时排查这个问题时一度怀疑是环境问题后来用调试器一看变量引用才发现arr[0]压根没指向任何对象。新手遇到空指针不要慌先检查引用有没有真正指向一个对象。4.2 length 到底是哪个长度length在二维数组里是个容易混淆的属性。arr.length是外层数组长度即行数arr[i].length是第 i 行内层数组长度即这一行的列数。常规矩形数组里所有arr[i].length都相等但不规则数组里就各不相同。还有一个面试经常问的String[][] s new String[2][3];里面的元素默认值是什么答案是 null因为这是引用类型数组。而int[][]是 0boolean[][]是 falsechar[][]是 \u0000。我复习时专门做了个表记下来面试的时候能脱口而出印象分会高不少。4.3 复制二维数组是浅拷贝这是个特别容易踩的坑。很多人以为int[][] copy arr.clone();就能复制整个二维数组其实只复制了外层数组的引用内层数组还是同一个对象。你改copy[0][0]arr[0][0]也跟着变因为两个外层数组里的copy[0]和arr[0]指向的是同一个内层数组。Arrays.copyOf和System.arraycopy也一样对二维数组都是浅拷贝只拷贝外壳。要真正的深拷贝得自己手动遍历int[][] copy new int[arr.length][]; for (int i 0; i arr.length; i) { copy[i] arr[i].clone(); }先用clone()复制每一行一维数组的 clone 是深拷贝复制的是实际元素再把新的一维数组放进外层。这样改copy就不会影响原数组了。用 Java 8 的流可以写得更简洁int[][] copy Arrays.stream(arr).map(int[]::clone).toArray(int[][]::new);但这种写法可读性差一点我复习的时候更喜欢用循环毕竟代码是写给未来的自己看的清晰最重要。5. 面试视角内存模型与高频编程题5.1 面试官爱问的内存问题复习二维数组如果只是为了应付考试那了解语法就够了。但你要是准备 Java 面试内存模型这块基本躲不掉。我复盘了一些常见问法第一个问题是Java 的二维数组在内存里是连续的吗答案是不是完全连续。外层数组是一块连续内存里面存引用每个内层数组又是一块独立连续内存但内层数组之间不保证在堆里相邻。所以严格说Java 二维数组是分段连续。第二个问题是遍历二维数组时按行遍历和按列遍历性能有区别吗这个值得展开。因为内层数组本身在内存中是连续的按行遍历时你依次访问arr[i][0]、arr[i][1]、arr[i][2]这些元素在同一个内层数组里地址连续CPU 缓存命中率高速度快。按列遍历时你访问arr[0][j]、arr[1][j]、arr[2][j]每次都要跳到不同的内层数组缓存命中率低性能差很多。我实测过一个 10000 x 10000 的 int 数组按行求和和按列求和耗时差距能有几倍到十几倍。这不是什么高级优化技巧而是理解内存模型后自然能得出的结论。面试时能说出这一层比单纯背答案强多了。5.2 和 C 语言二维数组的本质差异热词里提到 C 语言二维数组说明很多人在学 Java 之前学过 C或者面试时会被对比提问。我把两种语言在二维数组上的差异整理了一下对比项C 语言Java内存布局一整块连续内存外层数组存引用内层数组各自分配不保证连续数组本质本质是指针运算数组是对象有 length 属性是否可变栈上数组大小固定引用可以重新指向新数组越界检查不检查容易踩内存运行时检查抛异常传参方式退化为指向首元素的指针传的是数组引用内容修改会反映到原数组这个表格里的越界检查很重要。C 语言里int a[3][4]你访问a[5][2]可能编译不报错运行也不报错但内存已经被写坏了这是特别危险的事。Java 则会在运行时抛出ArrayIndexOutOfBoundsException保护了内存安全。我刚从 C 转到 Java 时还习惯性地不检查边界后来发现 Java 会帮我兜底但这种兜底不等于你可以随意越界异常处理本身也有成本。另一个差异是传参。C 语言里二维数组传参经常会退化成指针函数里拿不到数组长度信息。Java 里数组是对象自带length你可以在方法里直接访问不需要额外传长度参数。这一点 Java 确实方便不少。5.3 值得练的几道经典题复习二维数组光看语法不够得配合题目巩固。我自己整理了一个由易到难的题目清单每道题背后都有对应的知识点第一道是输出二维数组的螺旋顺序也就是从外圈到内圈螺旋打印所有元素。这道题考的是边界控制和方向切换新手一般要写半天但写完后对下标的理解会深很多。代码里有四个方向的变化每走完一圈要缩边界核心是记录上下左右四个边界值。int top 0, bottom matrix.length - 1; int left 0, right matrix[0].length - 1; while (top bottom left right) { for (int j left; j right; j) { System.out.print(matrix[top][j] ); } top; // 依次处理 右列、下边、左列 ... }第二道是旋转图像给一个 n x n 的矩阵原地顺时针旋转 90 度。经典做法是先沿主对角线翻转再左右翻转两步就能搞定。这道题考的就是前面讲的转置操作加上行内反转属于综合题。第三道是岛屿数量给定一个二维网格1 代表陆地0 代表水统计岛屿数量。这道题虽然要配合深度优先搜索但核心还是二维数组的遍历和边界判断。做这道题时我突然发现之前学的二维数组遍历、越界检查全都能用上有种原来学的东西是有用的的踏实感。如果时间有限优先做螺旋输出和旋转图像这两道题覆盖的知识点最密集性价比最高。6. 复习日记的复盘与个人建议把二维数组重新过了一遍之后我最大的感触是很多你以为会了的东西其实只是混了个眼熟。就像new int[3][]这种写法语法书上一带而过你当时没在意等到写杨辉三角、写不规则数组的时候就卡壳了。复习的真正价值就是把这种眼熟变成真会。我的做法是三步走第一步把内存图画一遍理解数组的数组到底长什么样第二步把创建、遍历、拷贝这些基础操作的代码亲手敲一遍敲完再用调试器观察变量的引用变化第三步找几道经典矩阵题练手螺旋输出、旋转图像、矩阵乘法这些题做完你对二维数组的掌控感会完全不同。还有一个特别实用的习惯每次写完二维数组相关代码都顺手检查三个点——arr[i].length有没有写对、行和列有没有搞反、拷贝是深是浅。这三个点是我踩坑频率最高的地方养成习惯后写代码和 debug 的速度都快了很多。复习期间我还发现把知识点写在纸上画出来比盯着屏幕记单词管用。二维数组这种概念性很强的内容视觉化记忆特别有效。我当时在纸上画了外层数组指向内层数组的箭头图隔几天就默画一遍到现在写代码时脑海里还留着那张图的样子。最后分享一个小技巧学习二维数组的时候可以顺手用二维数组实现一个简单的井字棋或者扫雷格子。不需要图形界面控制台输出就够了。这种小练习会让你觉得数组不是冷冰冰的语法而是能构建出真实逻辑的工具。我复习完之后做了一个井字棋的逻辑判断回头看二维数组的时候突然觉得它亲切多了。这也是我写这篇日记的初衷——把自己踩过的坑和想通的东西记下来下次再翻到的时候就是一笔实实在在的财富。