qinghuan
← 回到知识探索

后端现场 / 可下载小项目

System.arraycopy 抛异常后,目标数组为什么已经改了一部分?

捕获异常后再看目标:用类型失败、范围失败和暂存数组,弄清复制到底走到了哪一步。

展开本文目录
  1. 下载完整练习包
  2. 先看现场:第二个元素失败,第一个已经写进去了
  3. 把“复制到一半”说准确:第一个失败元素之前的前缀
  4. 同样是失败,范围错误和基本类型不匹配有不同结果
  5. 源数组里有异类元素,不代表这次请求一定会碰到它
  6. 如果类型失败时要保留旧目标,先在独立数组里完成检查
  7. 运行主程序:逐条核对 14 项检查
  8. 运行补充程序:8 项检查观察请求范围与 null
  9. 在业务代码里检查:失败后谁还会继续使用这个目标
  10. 记住这些要点
  11. 别把例子用过头
  12. 自己试一小步
  13. 查原始资料
开始前,会这些就够了
  • 会创建数组、读取下标,并使用 try/catch 捕获异常。
  • 知道 Object[] 中可以存放不同类型的对象;String[] 数组实际只接收可赋值给 String 的引用或 null。
  • 先读 数组:复制、边界与矩阵 的新容器与引用共享部分;本文专门补充复制失败后的状态。

你要把一批校验后的名称放进已有的 String[]。复制方法抛了 ArrayStoreException,你在 catch 里记录失败,准备继续使用旧数组,却发现第一个名字已经被新值覆盖。

下面把目标预先填成 old0、old1、old2。先故意在中途放入一个不合类型的元素,再检查目标,区分“这次调用失败”和“之前完全没做过写入”。所有实验只使用单线程和自己创建的数组。

把例子带到自己的电脑上

下载完整练习包

需要 JDK 17 或更新版本;2026-10-18 首次验证,2026-10-20 最终源码在 Windows / JDK 17.0.20.1 与 21.0.12 复跑。仅用标准库,无第三方依赖。仅使用标准库。实际运行日期、环境和范围见随包核验记录。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。

下载 ZIP · 7 个文件
包里有什么
  • ArraycopyFailureDemo.java:教学源码
  • ArraycopyFailureDemo.observed.stdout.txt:说明或核验记录
  • ArraycopyBoundaryProbe.java:教学源码
  • ArraycopyBoundaryProbe.observed.stdout.txt:说明或核验记录
  • README.md:说明或核验记录
  • verification.json:说明或核验记录
  • SHA256SUMS.txt:说明或核验记录

在这个文件夹打开 PowerShell,逐行运行

java -version
javac -version
javac -encoding UTF-8 --release 17 ArraycopyFailureDemo.java
java -cp . ArraycopyFailureDemo
javac -encoding UTF-8 --release 17 ArraycopyBoundaryProbe.java
java -cp . ArraycopyBoundaryProbe

解压到独立练习文件夹,在包含源码的目录打开终端,按下方顺序运行。示例运行记录不代表独立练习已完成;修改代码后需重新验证。

找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。

先看现场:第二个元素失败,第一个已经写进去了

源数组是 Object[]{"new0", 42, "new2"},目标是 String[]{"old0", "old1", "old2"}。调用 System.arraycopy(mixed, 0, target, 0, 3) 后,程序确实抛出 ArrayStoreException。这里的 42 作为对象数组元素,是一个 Integer 引用。

抓住异常后,目标实际变成 [new0, old1, old2]。new0 能赋值给 String,所以复制成功;处理 Integer 时不满足目标组件类型要求,复制在这里失败;new2 没有继续被复制。

这个结果直接推翻“只要抛异常,目标就还是旧值”的假设。即使 catch 后继续运行,库方法也不会自动替调用者恢复已覆盖的 old0。

把“复制到一半”说准确:第一个失败元素之前的前缀

实际限制来自目标数组对象的组件类型。把 String[] 放进一个 Object[] 变量,不会把那个数组改造成能装任意对象的数组;写入时仍会检查实际类型。JLS 10.5 解释了这种运行时存储检查。

System.arraycopy 的契约更具体:从请求的源区间开始数,假设第 k 个元素是第一个不能写入目标的元素,那么前 k 个元素已经复制到对应目标位置。k 可以为 0,也可以出现在后面;不能固定理解成恰好复制“一半”。

源位置 srcPos + i 对应目标位置 destPos + i。带偏移的实验从源索引 1 复制到目标索引 1:成功前缀改成 prefix,左边的 left、失败位置的 old2 和右边的 right 都保留。这让“哪些位置变化”能够逐项核对。

同样是失败,范围错误和基本类型不匹配有不同结果

先看反向对照:如果第一个被请求的引用元素就是 Integer,目标 String[] 没有可复制的成功前缀,因此保持原样。这仍然是元素类型失败,只是 k 等于 0。

再让 int[] 的请求长度超过数组范围,程序得到 IndexOutOfBoundsException,目标 [7, 8] 保持原值。范围错误时目标不变,是 arraycopy 文档单独写明的约定。

把 int[] 复制到 long[] 也会失败,目标 long[] 不变。普通赋值中 int 可以扩大为 long,但 arraycopy 不对两个基本类型数组逐个进行这种数值转换。

因此日志只有“复制失败”还不够。本文区分的是范围错误、基本类型数组不匹配和引用元素不兼容三类场景,没有穷举多个参数同时错误时的所有异常优先次序。

源数组里有异类元素,不代表这次请求一定会碰到它

对同一个 Object[]{"prefix", 42},只从索引 0 复制 1 个元素,调用成功。索引 1 恰好是请求右端点,区间不包含它。把长度从 1 改为 2 后,才会读到这个不兼容元素并失败。

同理,Object[]{42, "tail"} 从索引 1 开始只复制 1 个元素,也能成功。判断时要先写出实际源区间,而不是看见源数组中某处存在 Integer 就断定失败。

另两个边界要分开:null 元素可以写入引用类型数组;null 数组引用则是无效参数。补充实验确认 Object[]{null, "ok"} 能复制到 String[],但源数组为 null 时,即使长度为 0 仍抛 NullPointerException。

合法的空范围可以把 srcPos、destPos 分别放在两边数组的长度处,并指定 length=0;它成功返回且不改目标。这句话的前提是参数本身合法,并不是说零长度会绕过所有检查。

如果类型失败时要保留旧目标,先在独立数组里完成检查

本例先调用 Arrays.copyOf(mixed, mixed.length, String[].class),要求新数组的实际类型是 String[]。源元素不兼容时,这一步已经抛异常,后面向调用者旧目标复制的语句根本不会执行。

当所有元素都兼容时,再把已得到的 String[] 复制到已知长度足够、范围合法的旧目标中。两个分支都在主程序里实跑:暂存失败时旧目标保持 [old0, old1, old2];暂存成功时变为三个 new 值。

这多分配一个数组,也多了一次复制。它解决的是本例“元素类型不兼容时不要碰旧目标”的要求;它不是事务,也没有证明其他线程观察不到分步更新。若调用者目标范围本身不合法,仍需按接口要求处理。

另外,新容器保存的仍是对象引用。暂存只隔离这次目标写入,不会替业务复制每个对象内部的状态。不要把它写成“通用深复制”或“所有故障都能回滚”。

运行主程序:逐条核对 14 项检查

保存下面完整源码即可运行,不需要 Maven、网络或第三方库。检查用显式 AssertionError,未开启 -ea 也会执行。输出中每行数组都是完成相应调用或捕获异常后的真实目标内容。

2026 年 10 月 18 日在 Windows、Microsoft OpenJDK 17.0.20.1 与 21.0.12 上分别以 --release 17 编译运行;2026 年 10 月 20 日用相同两套 JDK 按本页最终源码复跑,输出一致。PASS 14 只统计这个主程序,不包含下一节的边界检查。

展开收起代码 · 小实验java · 79 行

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

小实验 · java
import java.util.Arrays;

/** Teaching example. Single-threaded; not a transaction or benchmark. */
public final class ArraycopyFailureDemo {
    private static int checks;

    private static void check(boolean condition, String message) {
        checks++;
        if (!condition) throw new AssertionError(message);
    }

    private static void expect(Class<? extends Throwable> type, Runnable action) {
        try {
            action.run();
        } catch (Throwable failure) {
            check(type.isInstance(failure), "Expected " + type + ", got " + failure);
            return;
        }
        throw new AssertionError("Expected " + type.getSimpleName());
    }

    public static void main(String[] args) {
        Object[] mixed = {"new0", 42, "new2"};
        String[] direct = {"old0", "old1", "old2"};
        expect(ArrayStoreException.class,
                () -> System.arraycopy(mixed, 0, direct, 0, mixed.length));
        check(Arrays.equals(direct, new String[]{"new0", "old1", "old2"}),
                "The assignable prefix must already have been copied");
        System.out.println("direct.type-failure=" + Arrays.toString(direct));

        Object[] offsetSource = {99, "prefix", 42, "tail"};
        String[] offsetTarget = {"left", "old1", "old2", "right"};
        expect(ArrayStoreException.class,
                () -> System.arraycopy(offsetSource, 1, offsetTarget, 1, 3));
        check(Arrays.equals(offsetTarget, new String[]{"left", "prefix", "old2", "right"}),
                "Only the successful prefix inside the requested destination range changes");
        System.out.println("offset.type-failure=" + Arrays.toString(offsetTarget));

        String[] firstFailure = {"old0", "old1"};
        expect(ArrayStoreException.class,
                () -> System.arraycopy(new Object[]{42, "ok"}, 0, firstFailure, 0, 2));
        check(Arrays.equals(firstFailure, new String[]{"old0", "old1"}),
                "An incompatible first element leaves a zero-length copied prefix");
        System.out.println("first-element.type-failure=" + Arrays.toString(firstFailure));

        int[] outOfBounds = {7, 8};
        expect(IndexOutOfBoundsException.class,
                () -> System.arraycopy(new int[]{1, 2}, 0, outOfBounds, 0, 3));
        check(Arrays.equals(outOfBounds, new int[]{7, 8}),
                "A range failure must not modify the destination");
        System.out.println("range-failure=" + Arrays.toString(outOfBounds));

        long[] primitiveMismatch = {7L, 8L};
        expect(ArrayStoreException.class,
                () -> System.arraycopy(new int[]{1, 2}, 0, primitiveMismatch, 0, 2));
        check(Arrays.equals(primitiveMismatch, new long[]{7L, 8L}),
                "Primitive arrays do not apply numeric widening during arraycopy");
        System.out.println("primitive-type-failure=" + Arrays.toString(primitiveMismatch));

        String[] stagedTarget = {"old0", "old1", "old2"};
        expect(ArrayStoreException.class, () -> {
            String[] staged = Arrays.copyOf(mixed, mixed.length, String[].class);
            // This statement is not reached when staging rejects an element.
            System.arraycopy(staged, 0, stagedTarget, 0, staged.length);
        });
        check(Arrays.equals(stagedTarget, new String[]{"old0", "old1", "old2"}),
                "Failed staging leaves the separate destination untouched");
        System.out.println("staged.type-failure=" + Arrays.toString(stagedTarget));

        Object[] compatible = {"new0", "new1", "new2"};
        String[] staged = Arrays.copyOf(compatible, compatible.length, String[].class);
        check(staged.getClass() == String[].class, "Staging uses the requested runtime array type");
        System.arraycopy(staged, 0, stagedTarget, 0, staged.length);
        check(Arrays.equals(stagedTarget, new String[]{"new0", "new1", "new2"}),
                "Successful staging permits the known-valid destination copy");
        System.out.println("staged.success=" + Arrays.toString(stagedTarget));
        System.out.println("PASS " + checks);
    }
}

怎样运行:保存为 ArraycopyFailureDemo.java,在该目录先单独执行 javac -encoding UTF-8 --release 17 ArraycopyFailureDemo.java;编译成功后另起一条命令执行 java -cp . ArraycopyFailureDemo。2026-10-18 首次在 Windows、Microsoft OpenJDK 17.0.20.1 和 21.0.12 分别编译运行;2026-10-20 按本页最终源码在两套 JDK 上复跑,每个版本仍通过 14 项检查,stdout 与下方输出按统一换行后相同。未测性能或并发可见性。

运行后对照结果
direct.type-failure=[new0, old1, old2]
offset.type-failure=[left, prefix, old2, right]
first-element.type-failure=[old0, old1]
range-failure=[7, 8]
primitive-type-failure=[7, 8]
staged.type-failure=[old0, old1, old2]
staged.success=[new0, new1, new2]
PASS 14

运行补充程序:8 项检查观察请求范围与 null

这份程序独立运行,保留同一源数组只改变请求长度的对照,再补上范围起点、合法空范围和 null 元素。不要把 null-element 输出与 null-array-empty-range 输出混为一类。

2026 年 10 月 18 日和 10 月 20 日,两套 JDK 每次各通过 8 项检查,输出一致。这里的 PASS 8 独立于主程序的 PASS 14;两份都运行后,每个 JDK 一共执行了 22 项检查,但不存在一份输出 PASS 22 的程序。

展开收起代码 · 小实验java · 61 行

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

小实验 · java
import java.util.Arrays;

/** Independent boundary checks for requested ranges and null values. */
public final class ArraycopyBoundaryProbe {
    private static int checks;
    private static void check(boolean condition, String reason) {
        checks++;
        if (!condition) throw new AssertionError(reason);
    }
    private static void expect(Class<? extends Throwable> type, Runnable action) {
        try {
            action.run();
        } catch (Throwable failure) {
            check(type.isInstance(failure), "Expected " + type + ", got " + failure);
            return;
        }
        throw new AssertionError("Expected " + type.getSimpleName());
    }
    public static void main(String[] args) {
        Object[] mixed = {"prefix", 42};
        String[] emptyTarget = {"old0", "old1"};
        // End indexes are legal when the length is zero.
        System.arraycopy(mixed, mixed.length, emptyTarget, emptyTarget.length, 0);
        check(Arrays.equals(emptyTarget, new String[]{"old0", "old1"}),
                "A valid empty range changes nothing");
        System.out.println("valid-empty-range=" + Arrays.toString(emptyTarget));

        String[] nullableTarget = {"old0", "old1"};
        System.arraycopy(new Object[]{null, "ok"}, 0, nullableTarget, 0, 2);
        check(Arrays.equals(nullableTarget, new String[]{null, "ok"}),
                "A null element is assignable to a reference component type");
        System.out.println("null-element=" + Arrays.toString(nullableTarget));

        String[] excludedTarget = {"old0", "old1"};
        System.arraycopy(mixed, 0, excludedTarget, 0, 1);
        check(Arrays.equals(excludedTarget, new String[]{"prefix", "old1"}),
                "The incompatible element at srcPos + length is excluded");
        System.out.println("incompatible-at-exclusive-end=" + Arrays.toString(excludedTarget));

        String[] includedTarget = {"old0", "old1"};
        expect(ArrayStoreException.class,
                () -> System.arraycopy(mixed, 0, includedTarget, 0, 2));
        check(Arrays.equals(includedTarget, new String[]{"prefix", "old1"}),
                "Extending the range includes the failing element after the copied prefix");
        System.out.println("same-source-longer-range=ArrayStoreException; "
                + Arrays.toString(includedTarget));

        String[] skippedHeadTarget = {"old0", "old1"};
        System.arraycopy(new Object[]{42, "tail"}, 1, skippedHeadTarget, 0, 1);
        check(Arrays.equals(skippedHeadTarget, new String[]{"tail", "old1"}),
                "An incompatible element before srcPos is also outside the request");
        System.out.println("incompatible-before-start=" + Arrays.toString(skippedHeadTarget));

        expect(NullPointerException.class,
                () -> System.arraycopy(null, 0, emptyTarget, 0, 0));
        check(Arrays.equals(emptyTarget, new String[]{"old0", "old1"}),
                "A null source array is invalid even for zero length");
        System.out.println("null-array-empty-range=NullPointerException");
        System.out.println("PASS " + checks);
    }
}

怎样运行:保存为 ArraycopyBoundaryProbe.java,在该目录先单独执行 javac -encoding UTF-8 --release 17 ArraycopyBoundaryProbe.java;编译成功后另起一条命令执行 java -cp . ArraycopyBoundaryProbe。2026-10-18 首次在 Windows、Microsoft OpenJDK 17.0.20.1 和 21.0.12 分别编译运行;2026-10-20 按本页最终源码在两套 JDK 上复跑,每个版本仍通过 8 项检查,stdout 与下方输出按统一换行后相同。这是独立边界程序,不包含主程序的 14 项。

运行后对照结果
valid-empty-range=[old0, old1]
null-element=[null, ok]
incompatible-at-exclusive-end=[prefix, old1]
same-source-longer-range=ArrayStoreException; [prefix, old1]
incompatible-before-start=[tail, old1]
null-array-empty-range=NullPointerException
PASS 8

在业务代码里检查:失败后谁还会继续使用这个目标

阅读调用点时,先找到目标数组的持有者。若它在复制前已经交给其他对象保存,catch 之后继续使用它就可能读到部分更新。异常处理策略必须说明这种状态能否接受。

再问本次允许的失败是什么:参数范围先检查、元素类型先检查、保持旧数组,还是允许部分写入。这里没有适合所有业务的同一种答案;接口应把失败后的可见状态说清楚。

本文确认了两个 JDK 上的单线程标准库行为,没有测试线程之间的发布方式、虚拟机故障、对象深复制或性能。既有数组基础文章讨论新容器和矩阵变换,本篇只把已有目标在复制失败后的状态补完整。

记住这些要点

  • 引用元素类型失败前,成功前缀可能已经写入目标;异常不自动意味着回滚。
  • 判断时同时看实际数组组件类型与请求区间,范围外的异类元素不参加本次逐元素复制。
  • null 元素、null 数组和合法零长度是三个不同边界。
  • 独立暂存可把本例的类型检查失败挡在旧目标之外,但不提供并发原子性或深复制。

别把例子用过头

  • 在 catch 中直接假设目标仍是调用前的内容。
  • 把 int 到 long 的普通数值转换规则套用到 int[] 到 long[] 的 arraycopy。
  • 只看整个源数组包含哪些类型,不计算 srcPos 与 length 决定的区间。
  • 把两份独立程序的计数或暂存策略的保证范围合并扩大。

自己试一小步

能否为带偏移的复制接口说明清楚:哪些输入成功,失败时哪些目标位置应保持不变?

  1. 自选一个父类型、两个子类型和 null,建立源数组与目标数组;先不运行,画出请求区间。
  2. 设计四组不同的 srcPos、destPos 和 length,其中包括空范围及刚好排除一个异类元素的范围。
  3. 逐组写下预测的异常类别、可能已写入的前缀和不应变化的位置,再运行核对。
  4. 为“类型不兼容时旧目标保持原样”的要求设计调用流程;加入偏移和目标空间不足的用例,并说明前提。
  5. 提交预测表、源码、实际输出和差异解释,不以一次成功样本代替边界检查。

做到这些,就算完成

  • 区间用不包含右端点的方式表达,没有把范围外元素算进去。
  • null 元素与 null 数组引用分别覆盖。
  • 每次核对整个目标,包括复制区间之外的位置。
  • 没有把单线程结果写成对并发读者的原子性承诺。

查原始资料

想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。

  • Java SE 17 System.arraycopy API (新标签页)↗Oracle Java 官方文档 · 英文

    区分失败原因,并明确引用元素类型失败前 [0,k) 范围已复制。

    2026-10-18 已实际打开并核读:arraycopy 方法的参数失败规则、基本类型不匹配、越界,以及首个不兼容引用元素下标 k 与已复制前缀的完整说明。 未声称通读整个文档或源码文件。

  • Java Language Specification 17, 10.5 Array Store Exception (新标签页)↗Oracle Java 官方文档 · 英文

    编译时可接受的数组引用关系不取消实际组件类型的运行时存储检查。

    2026-10-18 已实际打开并核读:10.5 的运行时组件赋值检查以及 Point[]/ColoredPoint[] 示例和解释。 未声称通读整个文档或源码文件。

  • Java SE 17 Arrays.copyOf(U[], int, Class) API (新标签页)↗Oracle Java 官方文档 · 英文

    暂存数组使用指定的运行时类型,并能在提交至调用者目标之前暴露不兼容元素。

    2026-10-18 已实际打开并核读:三参数泛型 copyOf 的新数组类型、共同索引范围和 ArrayStoreException 条件。 未声称通读整个文档或源码文件。