数组协变的类型安全漏洞:为什么String[]可以赋给Object[]
数组协变的类型安全漏洞:为什么String[]可以赋给Object[]
适读人群:Java中级开发者、对类型系统有深度兴趣的后端工程师 | 阅读时长:约12分钟 | 文章类型:原理剖析+安全分析
开篇故事
有次和一个同事讨论Java泛型时,他突然说了一句让我意外的话:"Java数组其实是有类型安全漏洞的,可以绕过编译器检查。"
我让他写出来看看。他写了这段:
String[] strings = new String[3];
Object[] objects = strings; // 编译通过!
objects[0] = "hello"; // 正常
objects[1] = 42; // 编译通过,但运行时...运行时:
java.lang.ArrayStoreException: java.lang.Integer
at Demo.main(Demo.java:5)我说:"这个我知道,叫数组协变,是Java设计上的一个历史决策。"
他问:"那为什么Java要这么设计?这不是个漏洞吗?"
这个问题很有意思。今天把数组协变的来龙去脉说清楚。
一、什么是数组协变
协变的定义
如果B是A的子类型,那么B[]是A[]的子类型,这叫做协变(Covariance)。
Java的数组就是协变的:
Integer[] ints = new Integer[3];
Number[] numbers = ints; // 合法!因为Integer是Number的子类
Object[] objects = ints; // 合法!因为Integer是Object的子类协变为什么有类型安全问题
协变让我们可以编写接受Object[]参数的通用方法,然后传入String[]:
public static void fill(Object[] array, Object value) {
Arrays.fill(array, value);
}
String[] strings = new String[3];
fill(strings, "hello"); // 可以
fill(strings, 42); // 编译通过,但运行时ArrayStoreException!问题就在这里:你传了一个String[],但方法里可以往里放任何Object,这破坏了数组的类型完整性。
二、核心原理深挖
JVM的运行时类型检查(ArrayStoreCheck)
Java的数组协变是有代价的。JVM在每次往数组里写入元素时,都会做一个运行时检查——叫做arraystore check:
objects[1] = 42; // JVM检查:42(Integer)是否可以存到这个数组里?
// 数组的实际类型是String[],Integer不是String,抛ArrayStoreException这是Java用运行时检查来弥补编译时类型安全损失的方式。
泛型设计吸取了教训
Java泛型在设计时,明确地不支持协变:
Integer[] ints = new Integer[1];
Number[] numbers = ints; // 合法(数组协变)
List<Integer> intList = new ArrayList<>();
List<Number> numberList = intList; // 编译错误!泛型不协变这是泛型比数组类型更安全的地方。泛型的"解决方案"是通配符(? extends/? super),把"可以读"和"可以写"的能力分开控制。
为什么Java要设计成数组协变
历史原因:Java最初没有泛型(JDK 1.5才有),为了能写出void sort(Object[] array)这样的通用方法,需要数组支持协变。如果当时不支持协变,就没法写通用的数组工具方法。
等Java有了泛型之后,数组协变就成了一个历史包袱。但改掉会破坏大量现有代码,所以保留至今。
执行流程
三、完整代码实现
代码一:数组协变的完整行为验证
package com.laozhang.trap.arraycovariance;
import java.util.Arrays;
/**
* 数组协变行为完整验证
* 展示协变的合法操作和类型安全漏洞
*/
public class ArrayCovarianceTest {
public static void main(String[] args) {
System.out.println("=== 基本协变赋值 ===");
String[] strings = new String[]{"hello", "world"};
Object[] objects = strings; // 合法:String[]是Object[]的子类型
System.out.println("strings类型: " + strings.getClass().getName());
System.out.println("objects类型: " + objects.getClass().getName()); // 仍然是[Ljava.lang.String;
System.out.println("是同一个对象: " + (strings == objects)); // true,不是副本
System.out.println("\n=== 安全读取(从Object[]读,实际是String)===");
Object first = objects[0]; // 安全,读出来是Object引用(实际是String)
System.out.println("读取: " + first + " 类型: " + first.getClass().getSimpleName());
System.out.println("\n=== ArrayStoreException触发 ===");
// 往String[]里存非String对象,运行时异常
try {
objects[0] = 42; // Integer不能存入String[]
} catch (ArrayStoreException e) {
System.out.println("ArrayStoreException: " + e.getMessage());
// 输出:java.lang.Integer
}
// 存String是可以的(String[]可以存String)
objects[1] = "new_value"; // 合法
System.out.println("存String成功: " + Arrays.toString(strings));
System.out.println("\n=== 多层继承的协变 ===");
Integer[] ints = {1, 2, 3};
Number[] numbers = ints; // Integer[] → Number[]
Object[] objs = ints; // Integer[] → Object[]
try {
numbers[0] = 3.14; // Double不能存入Integer[]
} catch (ArrayStoreException e) {
System.out.println("往Integer[]里存Double: " + e.getMessage());
}
try {
numbers[0] = Long.valueOf(100); // Long也不能存入Integer[]
} catch (ArrayStoreException e) {
System.out.println("往Integer[]里存Long: " + e.getMessage());
}
numbers[0] = Integer.valueOf(99); // Integer可以,合法
System.out.println("\n=== instanceof检查绕过协变问题 ===");
safeSet(strings, 0, "hello"); // 安全
safeSet(strings, 0, 42); // 不安全,打印警告而不是抛异常
System.out.println("\n=== 数组协变 vs 泛型不变 ===");
// 数组:协变,有ArrayStoreException风险
Number[] numArr = new Integer[3]; // 合法
// 泛型:不变,编译时保证安全
// List<Number> numList = new ArrayList<Integer>(); // 编译错误!
// 通配符:受控的协变
java.util.List<? extends Number> numList = new java.util.ArrayList<Integer>(); // 合法
// numList.add(42); // 编译错误!不能往? extends Number里add(因为编译器不知道具体类型)
Number n = numList.get(0); // 可以读
}
/**
* 安全写入数组(运行时检查类型)
*/
static void safeSet(Object[] array, int index, Object value) {
// 通过数组的componentType来检查
Class<?> componentType = array.getClass().getComponentType();
if (value != null && !componentType.isInstance(value)) {
System.out.println("警告:" + value.getClass().getSimpleName() +
" 无法存入 " + componentType.getSimpleName() + "[],跳过");
return;
}
array[index] = value;
System.out.println("成功存入: " + value);
}
}代码二:实际工作中的注意事项和正确做法
package com.laozhang.trap.arraycovariance;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* 数组协变在实际工作中的注意事项
*/
public class ArrayCovariancePractice {
/**
* 场景1:通用排序方法(协变的正当使用)
* Arrays.sort(Object[])就是利用协变实现的
*/
public static void sortStrings() {
String[] arr = {"banana", "apple", "cherry"};
Arrays.sort(arr); // 利用数组协变,String[]传给Comparable[]参数
System.out.println(Arrays.toString(arr));
}
/**
* 场景2:泛型方法替代数组协变(更安全)
*/
// 不安全版本:接受Object[],可能ArrayStoreException
@Deprecated
public static void fillUnsafe(Object[] array, Object value) {
Arrays.fill(array, value);
}
// 安全版本:用泛型,编译期保证类型安全
public static <T> void fillSafe(T[] array, T value) {
Arrays.fill(array, value); // 编译器保证value的类型和array元素类型一致
}
/**
* 场景3:避免把数组协变用作类型转换
*/
public static void avoidBadPattern() {
Object[] objects = new Integer[3];
// 编译通过,但这是不安全的模式
// 错误:可能被误用为真正的Object[]
fillUnsafe(objects, "string"); // ArrayStoreException
// 正确:用泛型
Integer[] ints = new Integer[3];
fillSafe(ints, 42); // 类型安全
}
/**
* 场景4:toArray()的泛型陷阱
*/
public static void toArrayTrap() {
List<String> list = new ArrayList<>();
list.add("hello");
list.add("world");
// 错误写法:运行时得到Object[],强转会ClassCastException
// String[] arr = (String[]) list.toArray(); // ClassCastException!
// 正确写法1:传入类型指定的数组
String[] arr1 = list.toArray(new String[0]);
System.out.println("正确1: " + Arrays.toString(arr1));
// 正确写法2(Java 11+):
String[] arr2 = list.toArray(String[]::new);
System.out.println("正确2: " + Arrays.toString(arr2));
}
/**
* 场景5:不要用数组作为方法的返回类型(协变的API设计问题)
*/
static class ItemStore {
private final String[] items = {"A", "B", "C"};
// 不好的设计:返回数组,调用方可以修改内部状态
@Deprecated
public String[] getItemsBad() {
return items; // 调用方可以 getItemsBad()[0] = "hacked"
}
// 好的设计:返回防御性拷贝,或者返回不可变List
public String[] getItemsSafe() {
return items.clone(); // 返回副本
}
public List<String> getItemsList() {
return List.of(items); // 不可变List(Java 9+)
}
}
public static void main(String[] args) {
System.out.println("=== 数组排序(正当使用)===");
sortStrings();
System.out.println("\n=== toArray陷阱 ===");
toArrayTrap();
System.out.println("\n=== 防御性拷贝 ===");
ItemStore store = new ItemStore();
String[] items = store.getItemsBad();
items[0] = "hacked"; // 修改了ItemStore内部状态!
System.out.println("内部被修改: " + store.getItemsBad()[0]); // hacked
ItemStore store2 = new ItemStore();
String[] safeItems = store2.getItemsSafe();
safeItems[0] = "safe_hack"; // 只修改了副本
System.out.println("内部未被修改: " + store2.getItemsSafe()[0]); // A
}
}四、踩坑实录
坑1:List.toArray()不指定类型,强转抛ClassCastException
报错现象:
java.lang.ClassCastException: class [Ljava.lang.Object; cannot be cast to class [Ljava.lang.String;
at com.example.service.DataService.getNames(DataService.java:23)触发代码:
List<String> names = new ArrayList<>();
names.add("Alice");
String[] arr = (String[]) names.toArray(); // 强转失败!根本原因:
List.toArray()(无参)返回的是Object[],不是String[]。即使List里只有String,运行时数组类型还是Object[],无法强转为String[]。
具体解法:
// 方式1:传入String[0]作为类型提示
String[] arr = names.toArray(new String[0]);
// 方式2:Java 11+
String[] arr = names.toArray(String[]::new);坑2:方法参数是Object[],意外存入了不兼容类型
报错现象:
java.lang.ArrayStoreException: java.lang.String
at java.base/java.lang.System.arraycopy(Native Method)
at com.example.util.ArrayUtils.merge(ArrayUtils.java:45)触发代码:
public static Object[] merge(Object[] a, Object[] b) {
Object[] result = Arrays.copyOf(a, a.length + b.length);
System.arraycopy(b, 0, result, a.length, b.length); // 如果a是Integer[], b是String[], 这里抛异常
return result;
}根本原因:
Arrays.copyOf(a, newLength)会创建一个和a相同类型的数组(如果a是Integer[],result也是Integer[])。往Integer[]里复制String[]的内容,ArrayStoreException。
具体解法:
// 明确创建Object[]类型的数组
public static Object[] merge(Object[] a, Object[] b) {
Object[] result = new Object[a.length + b.length]; // 明确是Object[]
System.arraycopy(a, 0, result, 0, a.length);
System.arraycopy(b, 0, result, a.length, b.length);
return result;
}坑3:暴露数组字段,调用方修改了内部状态
报错现象:
不是Exception,但内部数据被意外修改,业务逻辑出错,难以追踪。
触发代码:
public class Config {
private static final String[] ALLOWED_ROLES = {"ADMIN", "USER", "GUEST"};
public static String[] getAllowedRoles() {
return ALLOWED_ROLES; // 直接返回内部数组
}
}
// 调用方(可能无意识地修改了)
String[] roles = Config.getAllowedRoles();
roles[0] = "HACKER"; // 改了ALLOWED_ROLES!因为roles就是ALLOWED_ROLES具体解法:
// 返回防御性拷贝
public static String[] getAllowedRoles() {
return ALLOWED_ROLES.clone();
}
// 或者返回不可变List
public static List<String> getAllowedRoles() {
return List.of(ALLOWED_ROLES);
}五、总结与延伸
数组协变是Java早期的一个设计取舍,在没有泛型的年代,它让通用算法成为可能。但它确实引入了类型安全漏洞——JVM用运行时的ArrayStoreException来弥补。
几个记住就够的原则:
1. 数组协变是真实存在的,String[]可以赋给Object[]
2. 往协变后的数组里存不兼容类型,运行时ArrayStoreException
3. List.toArray()必须传入泛型数组,不然强转会ClassCastException
4. 不要把内部数组直接返回,用clone()或List.of()做防御性拷贝
5. 新代码优先用List<T>而不是T[],类型安全,API也更丰富
从这个例子也可以看出,Java泛型引入时为什么坚持"不可变"(invariant)的设计——就是吸取了数组协变的教训。代价是需要通配符(? extends/? super),但换来了编译时的类型安全。
