数组协变允许子类型数组赋值给父类型数组引用,如String[]可赋给Object[],但仅保证读安全、不保证写安全,导致ArrayStoreException在运行时发生;Java为兼容泛型前的集合操作引入该特性,而泛型容器采用不变性以确保编译期类型安全。

数组协变(Array Covariance)是 Java 和 C# 等语言中一个容易被忽视但影响深远的特性:子类型数组可以赋值给父类型数组引用。例如,String[] 可以赋给 Object[]。这看似方便,却在运行时破坏了类型安全——编译器无法完全阻止非法写入,导致 ArrayStoreException 在运行时才暴露问题。
为什么数组要协变?历史与兼容性权衡
Java 早期为支持泛型出现前的集合操作(如将不同类型的数组传给接受 Object[] 的方法),引入了数组协变。它让多态调用更“自然”,比如:
-
void printAll(Object[] arr)能接收String[]、Integer[]等 - 避免大量显式转型或包装,提升代码简洁性
- 但代价是牺牲了写操作的静态类型检查
协变如何引发运行时类型错误?
协变只允许“读安全”,不保证“写安全”。一旦通过父类型引用向数组写入不兼容元素,就会在运行时失败:
String[] strs = new String[2]; Object[] objs = strs; // 合法:协变赋值 objs[0] = new Integer(42); // 编译通过,但运行时抛出 ArrayStoreException
关键点在于:编译器只检查引用声明类型(Object[]),不追踪底层数组实际类型(String[])。因此写入检查被推迟到运行时,且无法提前预警。
与泛型容器的关键区别:擦除 vs 协变
泛型(如 ArrayList<String>)在 Java 中是**不变的(invariant)**:ArrayList<String> 不是 ArrayList<Object> 的子类型。这正是为了规避数组协变的风险:
ArrayList<String> list = new ArrayList<>();ArrayList<Object> objList = list; // 编译错误!类型不兼容- 所有 add 操作都在编译期校验,杜绝运行时类型污染
这种设计明确把类型安全责任交给编译器,而不是依赖运行时兜底。
如何规避风险?实用建议
除非必须与旧 API 交互,否则应优先使用泛型集合替代原始数组:
- 用
List<T>替代T[],尤其涉及多态传递或动态修改时 - 若必须用数组,避免通过父类型引用写入;读取时可放心利用协变
- 对不可变场景,考虑
Arrays.asList()包装或使用Collections.unmodifiableList() - 在需要协变语义又需类型安全时,可借助泛型方法 + 类型通配符(如
<T> void process(T[] arr))
本质上,数组协变是一种为便利牺牲安全的妥协。理解其边界,就能在合适的地方选择更健壮的抽象。







