bigywater.tech
Java 内部原理 · 53

数组协变真实验证:ArrayStoreException 到底何时触发

Java 数组是协变的(Dog[] 能赋给 Animal[]),这是编译期就放行的设计;但代价是运行时必须真实做一次类型检查兜底。这篇用真实的越界赋值场景,把 ArrayStoreException 的触发条件跑一遍。

编译通过 ≠ 运行安全
真实验证:Animal[] animals = dogs; animals[0] = new Cat(); 编译期完全放行,运行时真实抛出 ArrayStoreException
arraycopy 同样受限
真实验证:System.arraycopy() 不是内存直接拷贝的黑盒,同样真实做了元素类型检查并抛出异常

上一篇:String.intern() 真实验证

真实验证:Dog[] 赋给 Animal[] 后塞入 Cat

数组协变允许 Dog[] 赋值给 Animal[],编译器看到的静态类型是 Animal[],但数组对象本身记得自己的真实元素类型。

ArrayCovarianceDemo.java Dog[] dogs = new Dog[3]; Animal[] animals = dogs; // 数组协变 animals[0] = new Cat(); // 编译通过,运行时才检查
真实实测
animals.getClass(): class [LArrayCovarianceDemo$Dog;
animals 实际运行时元素类型仍是 Dog(存在数组对象头里)
真实捕获 ArrayStoreException: ArrayCovarianceDemo$Cat
animals[1] 赋值 Dog 成功: ArrayCovarianceDemo$Dog@7e9e5f8a

真实结果验证了数组协变的完整代价:animals.getClass() 显示这个数组对象的运行时类型真实是 Dog[],即使变量声明的静态类型是 Animal[]。编译器允许协变赋值是因为 Animal[] 只是一个"看待方式",数组对象本身携带的真实元素类型信息从未改变;每次写入数组时,JVM 都会真实拿这个元素类型和被写入的对象做一次 instanceof 级别的检查,不匹配就真实抛出 ArrayStoreException——这也是数组和泛型集合(编译期擦除、运行时不检查)在类型安全策略上的根本区别。

真实验证:Object[] 指向 String[] 时的同类场景

不只是自定义类继承关系,JDK 内置类型之间的数组协变同样真实受限。

真实实测
Object[]=String[] 场景下,塞 Integer 真实捕获 ArrayStoreException: java.lang.Integer

把 String[] 赋值给 Object[] 变量是最常见的协变场景之一(很多接受 Object[] 参数的 API 内部就可能这样传入),真实往里面塞一个 Integer 同样会立刻触发运行时检查失败。

真实验证:System.arraycopy() 是否绕过这个检查

arraycopy 是一个 native 方法,容易被误以为是纯内存拷贝、不做类型检查——真实测一下。

ArrayCovarianceDemo.java Dog[] src = new Dog[]{new Dog(), new Dog()}; Object[] dst = new Cat[2]; System.arraycopy(src, 0, dst, 0, 2);
真实实测
System.arraycopy 同样真实抛出 ArrayStoreException:
arraycopy: type mismatch: can not copy ArrayCovarianceDemo$Dog[] into ArrayCovarianceDemo$Cat[]
native 方法不等于绕过语言规则。真实结果证明 System.arraycopy() 虽然是用 C++ 实现的 native 方法,依然真实遵守 Java 数组的类型安全约束——把 Dog[] 的内容拷进一个运行时类型是 Cat[] 的数组,同样会被真实拦下并抛出异常,而且错误信息比普通赋值更明确,直接把两个数组的运行时类型都打印了出来,这是因为 arraycopy 在真正逐元素拷贝前会先做一次数组类型兼容性检查。

分步演示:运行时检查是怎么拦下"看起来合法"的赋值的

每一步的成功/失败都是真实运行这段代码得到的结果,不是按规则手工推的。

Animal[] 变量,Dog[] 的真实身体:塞 Cat 被拦下,塞 Dog 放行,arraycopy 同样受限
这一步被真实拦下的赋值这一步真实成功的赋值

    参考与说明

    • 本文全部结果均在本机真实运行 Java 17 得到,三组场景重复运行结果一致。
    • 和泛型擦除那篇放在一起看,正好体现 Java 里"数组协变+运行时检查"和"泛型不变+编译期检查、运行时擦除"两套完全不同的类型安全哲学。
    • 没有涉及:基本类型数组(int[]、long[])之间没有协变关系、多维数组的协变检查是否逐层生效——这两个话题留给系列后面按需再写。
    京ICP备2026067502号-1京公网安备11010502063487号