qinghuan
← Java SE / 全部笔记

单元 06 / Java SE

P0 · 必会主线L0 · 零基础

数组与二维数组:下标、遍历和复制边界

用一组成绩理解数组下标和固定长度,再分清共享同一数组、复制数组以及二维数组的区别。

本篇目录 · 按需要跳转

读完这篇,你能做什么

能遍历和复制数组,区分容器复制与对象复制,并处理二维数组边界。

学习理由、准备要求与知识点

为什么学它

数组是集合与算法学习的基础,复制和下标错误也常见于业务处理。

先读这些内容

知识点清单

  • 数组长度与合法下标
  • 元素默认值
  • 普通与增强遍历
  • 数组引用赋值
  • 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["完成所有元素后返回新矩阵"]

按顺序看,每一步做了什么

  1. 检查 null 与空数组的契约。
  2. 确认索引范围与组件类型。
  3. 决定共享、浅复制还是逐层复制。
  4. 先推导输出尺寸和坐标映射。
  5. 用非方阵、单行和非法结构检验边界。

03对照实现代码

数组复制如何决定截断与补零 (新标签页)↗

Microsoft OpenJDK 17.0.20.1;java.base/java/util/Arrays.java · java.util.Arrays#copyOf(int[],int)

以下为原创 Java 17 教学实现,不是原样 JDK 源码;只保留文中明确讨论的机制,省略优化、兼容及内部接口。

核心机制代码 · java
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 数组要隔离行元素,通常需要先新建外层,再对每一行单独复制;若元素本身是可变对象,还需设计其复制方式。对含环对象图深复制则需要身份映射,远比简单逐行复制复杂。

代码先调用谁,再调用谁

  1. copyOf 接收新长度
  2. 分配目标 int 数组
  3. 取源目标共同有效范围
  4. 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 + mn)。额外空间 O(mn) 输出空间,额外辅助状态 O(1)。

逐行校验花 O(m),每个元素搬运一次花 O(mn);当列数为零时仍需验证全部行,所以保留 O(m) 项。

保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。

展开收起代码 · Main.java · 完整可运行程序Java · 44 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

Main.java · 完整可运行程序 · Java
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 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。

这次练什么

能遍历和复制数组,区分容器复制与对象复制,并处理二维数组边界。

本篇重点加练

重点加练数组边界与复制后的共享关系,避免容量判断正确但数据所有权错误。

  • 数组长度与合法下标
  • 浅复制与元素共享

重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。

先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。

基础必做

  1. 作业 01代码推演

    分别创建 int[3] 与 String[2],为普通遍历和增强遍历画出访问步骤;对长度为零的数组列出仍然合法与不合法的操作。

    提交内容
    初始状态表、访问轨迹和独立验证代码。
    怎样算完成
    • 默认值与局部变量初始化区分
    • 下标边界完整

    练到的知识点数组长度与合法下标 · 元素默认值 · 普通与增强遍历

  2. 作业 02排错实验

    a=[2,4,6],b=a,c=Arrays.copyOf(a,5),依次修改 a 与 c 的元素;先画数组关系再执行,解释复制后的长度、数据和独立性如何验证。

    提交内容
    关系图、自写断言和运行记录。
    怎样算完成
    • 赋值与复制分开
    • 不同长度复制有检查

    练到的知识点数组引用赋值 · Arrays.copyOf

  3. 作业 03代码推演

    数组长度为 n 时,比较从左往右和从右往左遍历的初值、条件与推进;给出 n=0、1 的逐步轨迹并设计结束后不能再访问的下标。

    提交内容
    两种遍历表与边界核验。
    怎样算完成
    • 空数组不会被当作普通首轮
    • 条件与访问位置逐步对应

    练到的知识点数组长度与合法下标

综合应用

  1. 作业 04原理问答

    创建 Box[] 并复制数组,Box 包含可变 int 字段。设计两类修改实验,分别改变数组槽位和元素字段,说明完整隔离需要定义哪些合同。

    提交内容
    两组实验规格及所有权说明。
    怎样算完成
    • 容器独立与元素共享分别检验
    • 不把浅复制称为深复制

    练到的知识点浅复制与元素共享

  2. 作业 05编程实践

    实现不规则 int[][] 的每行求和,输入包含 [1,2]、空行、null 行和 [-3];自行定义 null 外层与 null 行政策,返回结果不修改输入。

    提交内容
    Java 实现、自写空结构/大值/不规则测试及运行证据。
    怎样算完成
    • 逐行长度独立处理
    • 输入不变性被验证
    • 溢出政策明确

    练到的知识点不规则二维数组 · 空数组与空行处理

  3. 作业 06面试场景

    统计方法把传入数组排序后返回最大值,调用者随后发现展示顺序变化。评审原接口的副作用合同,并为“允许修改”和“必须保留输入”两种需求设计测试。

    提交内容
    接口文档与前后状态验收表。
    怎样算完成
    • 副作用是否违约由合同判断
    • 只读需求有隔离证据

    练到的知识点数组长度与合法下标 · 普通与增强遍历 · 数组引用赋值 · 浅复制与元素共享

  4. 作业 07编程实践

    实现把 Box[] 指定半开区间复制到新数组的函数,明确 null 元素与元素是否共享;输入区间包含空区间、末尾位置和越界情况。

    提交内容
    Java 实现、自写索引/别名/输入保持测试与运行证据。
    怎样算完成
    • 区间及边界处理规则完整
    • 容器和元素两层关系分别验证

    练到的知识点数组长度与合法下标 · 浅复制与元素共享

  5. 作业 08设计评审

    日志快照保存对象数组,但每个对象的 message 后续仍可修改。定义快照按“当前字段值”还是“对象身份”保存,并给出可区分这两种合同的测试。

    提交内容
    快照需求与两组修改验收。
    怎样算完成
    • 所需复制深度来自合同
    • 不只检查数组引用不同

    练到的知识点浅复制与元素共享

进阶挑战 · 掌握前置后选做

  1. 作业 09编程实践

    完成方法和循环后:将不规则矩阵按行展开为一维数组,拒绝 null 行或制定替代政策;输出总长度计算需检查范围,至少用两种不同形状验证。

    提交内容
    Java 实现、自写边界和次序测试、运行证据。
    怎样算完成
    • 总容量与写入位置一致
    • 不同二维形状的相同内容可验证
    • 空间成本说明完整

    练到的知识点Arrays.copyOf · 不规则二维数组 · 空数组与空行处理

思维拓展

先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。

  1. 作业 10思维拓展 · 条件变化

    数组元素由整数改为可变对象,原复制和边界测试都保持通过。分析还有哪些需求可能没有被验证,并设计最少新增样本。

    提交内容
    测试缺口表和新增对象关系样本。
    怎样算完成
    • 数据结构变化与所有权变化相连
    • 每个新增检查有具体风险依据

    练到的知识点数组长度与合法下标 · 浅复制与元素共享

完成后,再检验一次

给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。

继续阅读

先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。

打开资料库,找下一份学习材料 ↗