读完这篇,你能做什么
能遍历和复制数组,区分容器复制与对象复制,并处理二维数组边界。
学习理由、准备要求与知识点
为什么学它
数组是集合与算法学习的基础,复制和下标错误也常见于业务处理。
先读这些内容
知识点清单
- 数组长度与合法下标
- 元素默认值
- 普通与增强遍历
- 数组引用赋值
Arrays.copyOf- 浅复制与元素共享
- 不规则二维数组
- 空数组与空行处理
运行前先确认Java 17;源码在本机 Microsoft OpenJDK 17.0.20.1 src.zip 核验,在线链接为上游 jdk-17.0.20-ga。深入模块可在掌握基础后回读。
01先弄懂它是什么
数组适合保存一组同类型的数据,例如 int[] scores = {80,90} 保存两项成绩。下标从 0 开始,所以只能用 0 和 1 访问,length 是 2。数组变量保存的是指向数组的引用;把它赋给另一个变量,只是让两个变量指向同一个数组,不会复制成绩。遇到“改一处、另一处也变了”,先检查是不是共用了数组。
数组创建后长度就固定了。要“扩容”,实际是创建更长的新数组、复制原有元素,再把变量改为指向新数组;其他仍指向旧数组的变量不会自动跟过来。基本类型数组默认填相应的零值,引用数组默认填 null。默认值不等于数据已填写,例如成绩 0 分和还没录入成绩,应由业务另行区分。
Java 的二维数组是数组的数组。int[][] grid 的外层元素是 int[] 引用,每一行可以长度不同,也可以为 null,所以不能只检查 grid[0].length 就认定整个结构是矩形。复制外层数组只复制行引用,修改共享行仍会相互影响。练习矩阵算法前,应明确是否只接受等长非空行、是否允许零行以及是否允许零列。
文中这几个词,先认一认
- 组件类型
- 数组中每一项允许保存的类型,例如
int[] 的组件类型是int。 - 浅复制
- 复制数组或集合里的值;如果值是引用,复制后仍可能共用同一个对象。
- 锯齿数组
- 每一行长度可以不同的二维数组,不能假定所有行和第一行一样长。
- 左闭右开区间
- 包含起点、不包含终点,例如 [2,5) 包含下标 2、3、4,长度是 3。
02看看它怎么工作
访问 a[i] 前需要保证数组引用非空且下标处于零到长度减一之间。遍历时采用 i < a.length,不能写成小于等于长度;空数组自然执行零次。增强 for 适合读取元素值,但循环变量赋新值不会替换原数组槽位。需要原地更新某个位置时,应使用下标并明确赋回 a[i]。
复制数组时,先确定新长度,再创建新数组,最后复制两边都放得下的部分。新长度较小时会截断;较大时,多出来的位置保持默认值。基本类型数组保存的是元素的值,复制后改一边的某个位置,不会影响另一边。对象数组复制的是引用,两个数组仍可能指向同一批元素对象,所以还要决定是否复制这些对象,以及哪些内部数据需要独立,不能靠一次通用复制就替业务作出决定。
矩阵变换适合先推导坐标映射,再写循环。一个 m 行 n 列矩阵顺时针旋转后有 n 行 m 列,原坐标 (r,c) 映射到 (c,m-1-r)。先用四个角检查方向,再检查长方形而不仅是正方形。图中的新矩阵表示独立行容器,避免把视图、共享行和真正复制混为同一种返回方式。
正在加载流程图…
查看流程图源码(Mermaid)
flowchart TD
A["接收二维数组"] --> B["校验每一行非空且等长"]
B --> C["原矩阵 m 行 n 列"]
C --> D["创建 n 行 m 列新矩阵"]
D --> E["原 r,c 写入 c,m-1-r"]
E --> F["完成所有元素后返回新矩阵"]按顺序看,每一步做了什么
- 检查
null与空数组的契约。 - 确认索引范围与组件类型。
- 决定共享、浅复制还是逐层复制。
- 先推导输出尺寸和坐标映射。
- 用非方阵、单行和非法结构检验边界。
03对照实现代码
Microsoft OpenJDK 17.0.20.1;java.base/java/util/Arrays.java · java.util.Arrays#copyOf(int[],int)
以下为原创 Java 17 教学实现,不是原样 JDK 源码;只保留文中明确讨论的机制,省略优化、兼容及内部接口。
public final class SourceModel {
public static int[] copy(int[] input, int length) {
if (input == null) throw new NullPointerException("input");
if (length < 0) throw new NegativeArraySizeException();
int[] output = new int[length];
int sharedLength = Math.min(input.length, length);
// 只复制两边都存在的槽位;剩余新槽位保持零。
for (int i = 0; i < sharedLength; i++) output[i] = input[i];
return output;
}
}真实 Arrays.copyOf(int[],int) 新建目标数组,再把较短长度范围交给 System.arraycopy。教学模型用显式循环展示相同的普通成功路径,并提前校验参数,异常同时出现时的检查次序不保证与真实方法相同。System.arraycopy 的底层实现不在这个 Java 模型中,不能把循环性能当作库方法性能。
复制长度选择两边长度的较小值,保证既不读出原数组范围,也不写出目标范围。多出的目标元素之所以为零,是因为 Java 数组创建时的初始化规则,而不是复制循环在后面额外填了零。截断时后面的原元素仍留在原数组里,只是不出现在新容器中。
对象数组在运行时还记得自己允许存什么类型,复制时也会检查。数组允许某些父子类型之间的赋值,但真放入不兼容元素时仍可能抛 ArrayStoreException。因此不能把 int[] 复制模型直接推广成任意泛型数组创建办法;数组的运行时检查和泛型的编译检查,会在后续文章分别比较。
这一源码路径说明新容器何时出现,却不决定业务上该复制几层。二维 int 数组要隔离行元素,通常需要先新建外层,再对每一行单独复制;若元素本身是可变对象,还需设计其复制方式。对含环对象图深复制则需要身份映射,远比简单逐行复制复杂。
代码先调用谁,再调用谁
copyOf接收新长度- 分配目标
int数组 - 取源目标共同有效范围
System.arraycopy完成范围复制并返回新容器
04动手写一个例子
旋转矩形矩阵并隔离输入
把矩形 int[][] 顺时针旋转九十度,返回新矩阵。拒绝 null 外层、null 行与不等长行;空外层返回空矩阵,允许零列。
- 输入 1
[[1,2,3],[4,5,6]]- 输出 1
[[4,1],[5,2],[6,3]]
- 输入 2
[[7]]- 输出 2
[[7]]
- 输入 3
[]- 输出 3
[]
先完成结构验证再分配输出,否则处理到半途才发现锯齿行会留下难以解释的部分结果。零行时没有第一行可以读取,因此直接返回空矩阵。存在行时以第一行长度确定列数,并逐行比较;零列合法,输出有零行,Java 的空外层本身不保存另一维尺寸。
输出尺寸来自坐标映射:源列成为目标行,源行反向排列成为目标列。对每个 r 和 c 写一次 result[c][rows-1-r]。源坐标不同就不会映射到同一个目标槽位,所有目标槽位又恰好都有来源,因此不需要冲突处理或额外访问标记。
使用新数组使返回结果与输入行容器隔离,旋转后改结果不会改输入。算法不做原地交换,所以支持长方形并保持实现清晰。测试必须包含长方形,因为错误地假设行列相等的代码经常能通过所有正方形样本。另用一个修改返回值的断言验证复制边界,而不只比较首次输出。
为什么这样写是对的处理到源位置之前,所有已处理源元素都已写入唯一正确目标坐标,未处理位置尚未贡献。
逐行校验花 O(m),每个元素搬运一次花 O(mn);当列数为零时仍需验证全部行,所以保留 O(m) 项。
保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。
展开收起代码 · Main.java · 完整可运行程序
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
import java.util.Arrays;
public class Main {
static int[][] rotate(int[][] matrix) {
if (matrix == null) throw new IllegalArgumentException("matrix");
int rows = matrix.length;
if (rows == 0) return new int[0][0];
if (matrix[0] == null) throw new IllegalArgumentException("null row");
int cols = matrix[0].length;
// 所有行先通过矩形校验,再开始写输出。
for (int[] row : matrix)
if (row == null || row.length != cols)
throw new IllegalArgumentException("not rectangular");
int[][] result = new int[cols][rows];
for (int r = 0; r < rows; r++)
for (int c = 0; c < cols; c++)
result[c][rows - 1 - r] = matrix[r][c];
return result;
}
static void check(int[][] expected, int[][] input) {
if (!Arrays.deepEquals(expected, rotate(input)))
throw new AssertionError(Arrays.deepToString(input));
}
static void reject(int[][] input) {
try { rotate(input); }
catch (IllegalArgumentException e) { return; }
throw new AssertionError("must reject");
}
public static void main(String[] args) {
// 非方阵、单项、空矩阵与零列都固定预期。
check(new int[][]{{4,1},{5,2},{6,3}},
new int[][]{{1,2,3},{4,5,6}});
check(new int[][]{{7}}, new int[][]{{7}});
check(new int[0][0], new int[0][0]);
check(new int[0][0], new int[][]{{},{}});
reject(new int[][]{{1},{2,3}});
reject(new int[][]{null});
reject(null);
int[][] original = {{8}};
int[][] copied = rotate(original);
copied[0][0] = 9; // 验证结果不共享输入行。
if (original[0][0] != 8) throw new AssertionError("alias");
System.out.println("PASS 8");
}
}这些特殊情况也要试一试
- 零行时不能读取第零行。
- 零列矩阵仍需逐行校验。
- 锯齿行与
null行均拒绝。 - 新矩阵修改不影响原行。
进阶选读 · 可选深入:原地正方形旋转与缓存局部性
场景大型正方形矩阵希望减少额外存储。
做法对方阵先沿主对角线交换,再逐行反转;或按四元循环交换。按块处理有助于改善访问局部性,但应先验证坐标不变量。
复杂度原地方案 O(n²) 时间与 O(1) 辅助空间。
好处与代价会修改调用者数据,且不直接适用于一般长方形;节省空间换来了更复杂的接口与交换推理。
05项目里怎么用
数组适合固定大小、索引访问密集的数据。对动态追加业务,直接使用集合通常更容易维护;自己扩容需要处理增长策略、长度溢出与复制成本。接口返回数组时应明确调用者是否拥有修改权;若内部状态不能被修改,返回副本比只把字段声明 final 更有意义。
二维结构既可用数组的数组,也可用一维数组配合行列尺寸。后者位置计算集中、布局更连续,但更容易出现乘法和索引错误;前者可表达锯齿结构,矩形约束则要自己验证。选型要看数据是否天然规则,以及算法访问模式是否需要统一存储。
复制能减少意外共享,也会花时间和内存。反复复制巨大数组会增加分配与 GC 压力,返回共享视图又可能让其他线程改动数据。可以在交接数据时复制一次,并明确之后由谁修改,而不是每一层都复制。优化前先确认二维数组的行和其中对象没有被意外共用。
容易踩的坑
- 用 i <= length 遍历数组。
- 把外层 clone 误称二维深复制。
- 只检查第一行便假设矩阵规则。
- 在增强 for 中给循环变量赋值,误以为改了原槽位。
几种做法,怎么选
表格有四列,窄屏可左右滑动查看代价和适用条件。
| 方案 | 优势 | 代价与限制 | 适用条件 |
|---|---|---|---|
| 直接共享数组 | 避免复制成本 | 修改传播且所有权难管理 | 明确共享和生命周期的内部代码 |
| 边界防御性复制 | 隔离调用者与内部状态 | 分配及搬运成本 | 公开 API 与不可变值对象 |
| 一维数组加尺寸 | 布局紧凑且索引统一 | 需自行维护坐标映射 | 规则矩阵与性能敏感计算 |
06检验是否读懂了
先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。
先用自己的话解释
01数组变量赋值为何不是复制?
展开参考答案与推导
赋值复制引用值,两变量指向同一个数组对象。之后修改其中一个槽位会通过共享对象可见;只有显式创建新数组并搬运元素才获得独立容器。
02二维 int 数组复制外层后为什么仍互相影响?
展开参考答案与推导
外层元素是行数组引用,浅复制保留这些引用。需要隔离行内数值时应为每一行新建数组;复制多少层取决于结构,不是看到 clone 就能推断。
03为什么零列时复杂度仍有 O(m)?
展开参考答案与推导
算法必须验证每一行存在且长度一致,即使没有任何元素需要搬运,行检查仍进行 m 次。把复杂度只写 O(mn) 会在 n 为零时隐藏这部分工作。
换个情况,还会处理吗
01返回数组的字段是 final,还需要复制吗?
展开参考答案与推导
final 只限制字段重新绑定,不限制数组槽位修改。若不能让调用者修改内部状态,仍应返回副本或更合适的只读接口,并说明元素是否也可变。
02矩阵旋转只在长方形报错,先查哪里?
展开参考答案与推导
先检查输出是否分配为列数行、行数列,以及映射是否使用 rows-1-r。只用正方形测试会掩盖行列混用,增加二乘三和一乘多的样本。
03大数组复制很慢,可以直接共享吗?
展开参考答案与推导
先确认调用者是否会修改以及能否约定所有权转移。共享会改变接口语义,不能只为了减少分配就忽略并发和修改传播;先测实际热点再选择边界复制或专用视图。
写代码试一试
01为单行旋转添加测试
- 输入 / 条件
[[1,2,3]]- 输出 / 预期
[[1],[2],[3]]
提示 源列成为目标行。
展开参考答案与推导
加入目标三行一列的断言,验证非方阵的极端形状。每项目标列都是零,因为源行数为一;这也能发现把 rows 和 cols 混用的错误。
02实现二维 int 矩阵独立复制
- 输入 / 条件
[[1],[2,3]]- 输出 / 预期
同值且行不共享
提示 这里允许锯齿行。
展开参考答案与推导
新建等长外层,逐行调用 Arrays.copyOf,null 行按事先契约保留或拒绝。测试修改复制结果某一行后原行不变;不要无意给复制方法加上矩形限制。
03原地反转一维数组
- 输入 / 条件
[1,2,3,4]- 输出 / 预期
[4,3,2,1]
提示 左右指针向中间移动。
展开参考答案与推导
每轮交换 a[left] 与 a[right] 后同时推进,条件为 left < right。空数组与单元素不交换,偶数和奇数长度都应覆盖,时间 O(n)、辅助空间 O(1)。
读完之后,独立完成
课后作业:自己做一遍
本篇共 10 道作业,先完成 8 道基础与综合任务,再按前置知识挑战进阶题。其中包含 1 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。
能遍历和复制数组,区分容器复制与对象复制,并处理二维数组边界。
本篇重点加练
重点加练数组边界与复制后的共享关系,避免容量判断正确但数据所有权错误。
- 数组长度与合法下标
- 浅复制与元素共享
重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。
先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。
基础必做
- 作业 01代码推演
分别创建
int[3] 与String[2],为普通遍历和增强遍历画出访问步骤;对长度为零的数组列出仍然合法与不合法的操作。- 提交内容
- 初始状态表、访问轨迹和独立验证代码。
- 怎样算完成
- 默认值与局部变量初始化区分
- 下标边界完整
练到的知识点数组长度与合法下标 · 元素默认值 · 普通与增强遍历
- 作业 02排错实验
a=[2,4,6],b=a,c=
Arrays.copyOf(a,5),依次修改 a 与 c 的元素;先画数组关系再执行,解释复制后的长度、数据和独立性如何验证。- 提交内容
- 关系图、自写断言和运行记录。
- 怎样算完成
- 赋值与复制分开
- 不同长度复制有检查
练到的知识点数组引用赋值 · Arrays.copyOf
- 作业 03代码推演
数组长度为 n 时,比较从左往右和从右往左遍历的初值、条件与推进;给出 n=0、1 的逐步轨迹并设计结束后不能再访问的下标。
- 提交内容
- 两种遍历表与边界核验。
- 怎样算完成
- 空数组不会被当作普通首轮
- 条件与访问位置逐步对应
练到的知识点数组长度与合法下标
综合应用
- 作业 04原理问答
创建 Box[] 并复制数组,Box 包含可变
int字段。设计两类修改实验,分别改变数组槽位和元素字段,说明完整隔离需要定义哪些合同。- 提交内容
- 两组实验规格及所有权说明。
- 怎样算完成
- 容器独立与元素共享分别检验
- 不把浅复制称为深复制
练到的知识点浅复制与元素共享
- 作业 05编程实践
实现不规则
int[][] 的每行求和,输入包含 [1,2]、空行、null行和 [-3];自行定义null外层与null行政策,返回结果不修改输入。- 提交内容
- Java 实现、自写空结构/大值/不规则测试及运行证据。
- 怎样算完成
- 逐行长度独立处理
- 输入不变性被验证
- 溢出政策明确
练到的知识点不规则二维数组 · 空数组与空行处理
- 作业 06面试场景
统计方法把传入数组排序后返回最大值,调用者随后发现展示顺序变化。评审原接口的副作用合同,并为“允许修改”和“必须保留输入”两种需求设计测试。
- 提交内容
- 接口文档与前后状态验收表。
- 怎样算完成
- 副作用是否违约由合同判断
- 只读需求有隔离证据
练到的知识点数组长度与合法下标 · 普通与增强遍历 · 数组引用赋值 · 浅复制与元素共享
- 作业 07编程实践
实现把 Box[] 指定半开区间复制到新数组的函数,明确
null元素与元素是否共享;输入区间包含空区间、末尾位置和越界情况。- 提交内容
- Java 实现、自写索引/别名/输入保持测试与运行证据。
- 怎样算完成
- 区间及边界处理规则完整
- 容器和元素两层关系分别验证
练到的知识点数组长度与合法下标 · 浅复制与元素共享
- 作业 08设计评审
日志快照保存对象数组,但每个对象的 message 后续仍可修改。定义快照按“当前字段值”还是“对象身份”保存,并给出可区分这两种合同的测试。
- 提交内容
- 快照需求与两组修改验收。
- 怎样算完成
- 所需复制深度来自合同
- 不只检查数组引用不同
练到的知识点浅复制与元素共享
进阶挑战 · 掌握前置后选做
- 作业 09编程实践
完成方法和循环后:将不规则矩阵按行展开为一维数组,拒绝
null行或制定替代政策;输出总长度计算需检查范围,至少用两种不同形状验证。- 提交内容
- Java 实现、自写边界和次序测试、运行证据。
- 怎样算完成
- 总容量与写入位置一致
- 不同二维形状的相同内容可验证
- 空间成本说明完整
练到的知识点Arrays.copyOf · 不规则二维数组 · 空数组与空行处理
思维拓展
先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。
- 作业 10思维拓展 · 条件变化
数组元素由整数改为可变对象,原复制和边界测试都保持通过。分析还有哪些需求可能没有被验证,并设计最少新增样本。
- 提交内容
- 测试缺口表和新增对象关系样本。
- 怎样算完成
- 数据结构变化与所有权变化相连
- 每个新增检查有具体风险依据
练到的知识点数组长度与合法下标 · 浅复制与元素共享
完成后,再检验一次
给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。
继续阅读
先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。
- 必读规范英文
Java 17 语言规范:数组 (新标签页)↗
核对数组创建、长度、运行时存储检查和多维数组的类型规则。
- 实践参考文档英文
Java 17 API:Arrays (新标签页)↗
查阅
copyOf、equals、sort、binarySearch和asList的具体契约。 - 深入规范英文
Java 17 语言规范:类型、值与变量 (新标签页)↗
核对基本类型范围、引用类型、类型变量与可具体化类型,理解编译期约束。
- 实践实践指南英文
Java 代码可视化:逐行看变量、引用和方法调用 (新标签页)↗
先写两个变量指向同一个数组,再通过其中一个变量修改数组。运行前画出你预计的关系,运行后逐步核对;再练一次传参和一次递归。只使用可视化也能完成这些练习。 可视化功能免费,页面为英文且有广告。只适合短小示例,不支持完整的多文件项目;页面带 AI 问答功能,输入代码会交给外部服务处理,练习请用不含私密信息的小例子。
- 深入参考文档英文
OpenJDK 17u java.util.Arrays (新标签页)↗
定位
int版copyOf的新数组、共同范围与 arraycopy 调用。 - 深入参考文档英文
Java17 System.arraycopy (新标签页)↗
了解范围复制、重叠与类型检查契约。