Java-Generics-PartII

泛型擦除

Java泛型是编译时compile time)行为,在运行时runtime)所有与泛型相关的信息都会被彻底擦除。擦除后,所有参数化类型都会统一还原为同一个原始类型raw type)。这里引入一个新术语——原始类型,指的是泛型类 / 接口去掉所有泛型类型参数后的基础类型,也是泛型类型经过类型擦除后,JVM在运行时实际识别的类型。

1
2
3
4
5
System.out.println("runtime type of ArrayList<String>: "+new ArrayList<String>().getClass());
System.out.println("runtime type of ArrayList<Long> : "+new ArrayList<Long>().getClass());

prints: runtime type of ArrayList<String> : class java.util.ArrayList
runtime type of ArrayList<Long> : class java.util.ArrayList

明确泛型是编译时行为,是理解泛型诸多特性的关键

泛型类中允许定义静态成员,例如静态方法、静态变量、静态内部类等。由于静态成员隶属于类而非对象,而所有参数化类型在运行时都会因类型擦除退化为原始类型,因此它们会共享同一个静态成员实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) {
// 实例化1:GenericClass<String>
GenericClass<String> strObj = new GenericClass<String>();
strObj.staticField = 10; // 修改静态字段

// 实例化2:GenericClass<Integer>
GenericClass<Integer> intObj = new GenericClass<Integer>();
// 访问静态字段:值已被strObj修改,说明静态字段只有一份
System.out.println(intObj.staticField); // 输出:10

// 再次修改静态字段
GenericClass.staticField = 20;
// strObj访问静态字段:值同步变化
System.out.println(strObj.staticField); // 输出:20
}

同理,引用泛型类的静态变量时,应使用原始类型而非参数化类型。

1
2
GenericClass<Integer>.staticField; // 错误,语法不合法
GenericClass.staticField; // 正确

上一篇提到,泛型类型需先指定具体类型,成为参数化类型后才能使用。这里的使用,指的是像非泛型类一样出现在代码的各个位置,常见场景包括作为方法参数、返回值、局部变量类型、成员变量类型,以及通过new关键字实例化对象等。

但相较于非泛型类,参数化类型存在一些使用限制:无法创建泛型数组、无法作为异常类型、无法作为类型字面量(如List<String>.class)、无法用于instanceof关键字的类型判断。

无法作为类型字面量的原因很好理解。受泛型擦除特性影响,List<String>.classList<Integer>.class最终都会指向List.class。而Java类型系统明确规定:一个类(原始类型)在JVM中仅存在一个Class实例。参数化类型的字面量既无实际意义,还会导致类型标识混乱,因此Java在编译期就直接禁止了这种语法。

无法作为异常类型和instanceof关键字的判断类型,本质原因也与此相同。异常捕获和instanceof判断均为运行时行为:JVM在程序运行中捕获异常对象后,需匹配对应的catch块处理,这个过程依赖JVM对异常具体类型的精准识别。Exception<String>Exception<Integer>在运行时会因泛型擦除完全无区别,JVM无法区分泛型异常类型的不同参数化实例,自然也无法在catch块中匹配对应的参数化类型。即便允许定义泛型异常,运行时也无法达到预期效果,因此Java从语法层面彻底禁止了这类用法。

协变

在解释为何不能创建参数化类型数组前,我们先做一个小测验:以下类型中,哪些可以相互进行类型转换(cast)?

List<String>、List<Object>、List<?>、Collection<String>、Collection<Object>、Collection<?>

提到类型转换,总体可分为基本数据类型转换、引用类型转换、自动装箱/拆箱及特殊场景转换。本测验中的类型转换特指引用类型转换,具体包括向上转型upcast)和向下转型downcast)。

向上转型是引用类型转换的特殊且简单形式,特指在类的继承体系中,将子类类型转换为父类类型(包括直接父类、间接父类,以及子类实现的接口类型),是Java多态特性的核心实现方式。

向上转型属于隐式类型转换,无需手动编写转换语法,由编译器自动完成,且转换过程绝对安全,不会抛出运行时异常。

1
2
3
4
Dog dog = new Dog();
// 向上转型(upcast):子类Dog → 父类Animal
// 隐式转换,无需手动写 (Animal),编译·器自动完成
Animal animal = dog;

向下转型则是将指向子类对象的父类引用,重新赋值给子类引用,这种转换需要显式手动声明(添加(子类类型))。

1
2
3
4
5
6
Animal animal = new Cat();

if (animal instanceof Cat) { // 先用instanceof校验,避免异常
// 向下转型(downcast)
Cat cat = (Cat) animal;
}

显然,上述测验的核心是判断多个参数化类型之间是否存在继承关系。

先看List<String>List<Object>:这两个参数化类型不存在任何继承关系。这一点可能与直觉相悖——毕竟StringObject的子类,从语义上看,储存String的容器似乎也可看作储存Object的容器。这种直觉对应的特性在类型系统中被称为协变covariance),即容器类型的继承关系容器内元素类型的继承关系保持同向一致。简单来说:若子类类型可替换父类类型,那么“装载子类的容器”也可替换“装载父类的容器”,这就是协变。

Java中的数组支持协变,子类数组可直接赋值给父类数组(隐式向上转型),但这种特性存在运行时风险。

1
2
Animal[] animalArr = new Dog[3]; 
animalArr[0] = new Dog(); // 合法:向Dog[]中放入Dog对象

这是因为数组是具体化类型Reifiable Type),运行时会完整保留其元素类型信息。即便进行了协变赋值,数组的真实类型也不会丢失,JVM能精准识别数组的实际组件类型(元素类型)。每次向数组存入元素时,JVM都会对比待添加元素的实际类型与数组的真实组件类型,这个过程称为数组存储检查(array store check),若类型不兼容则直接抛出ArrayStoreException,确保数组内元素的同质性(homogenous),从源头阻止错误扩散。

1
2
3
4
Animal[] arr = new Dog[2];
// 运行时能获取数组的真实元素类型:Dog
System.out.println(arr.getClass().getComponentType());
arr[0] = new Cat(); // 运行时即时抛ArrayStoreException,错误可快速定位

Java泛型本质是编译时语法糖,编译后会执行类型擦除:所有参数化类型(如List<Animal>、List<Dog>)都会被擦除为原始类型ListJVM在运行时无法区分List<Dog>List<Cat>,自然无法像数组那样校验元素类型。因此,若允许泛型协变,会导致容器内元素失去同质性,引发类型安全问题。

1
2
3
4
// 假设允许泛型协变
List<Animal> list = new ArrayList<Dog>();
list.add(new Cat()); // 编译期合法(List<Animal>理论可装Cat)
Dog dog = (Dog) list.get(0); // 运行时抛ClassCastException,错误根源难定位

Java数组的协变特性中,ArrayStoreException的报错行就是错误行为的发生行(即向Dog数组中存入了Cat)。而泛型若允许协变,错误要等到获取元素并强转时才会暴露,ClassCastException的报错行与错误根源行脱节,排查难度大幅增加。基于类型安全的核心目标,Java禁止泛型支持协变。

回到最初的测验,各类参数化类型的转换规则如下表所示。

转换方向 →
源类型 ↓
List<String> List<Object> List<?> Collection<String> Collection<Object> Collection<?>
List<String>
List<Object>
List<?>
Collection<String>
Collection<Object>
Collection<?>

再来看List<String>Collection<String>:这两个参数化类型存在继承关系。总体而言,参数化类型的继承关系需满足以下条件:

  1. 原始类型之间必须存在继承关系;
  2. 类型实参需完全相同,或目标参数化类型使用通配符且源类型的类型实参符合通配符约束。

关于通配符的详细内容,限于篇幅,将在下一篇中展开说明。

回到核心话题——为何不允许创建元素类型为参数化类型的数组。由于Java数组支持协变,若允许创建List<String>[],则可将其协变为Object[]。但受类型擦除影响,数组仅能识别元素类型为List,无法感知泛型参数String,因此可向数组中存入其他泛型参数的List(如List<Integer>),导致数组元素失去同质性。当从数组中取出元素并强转为List<String>时,就会抛出ClassCastException。因此,Java禁止创建泛型数组,本质是为了规避这种类型安全风险。

1
2
3
4
5
6
7
8
9
10
// 编译报错:Generic array creation(泛型数组创建不被允许)
List<String>[] stringListArray = new List<String>[5];

// 补充:即使通过强制类型转换“绕过”编译,运行时也会有风险(不推荐)
List<String>[] unsafeArray = (List<String>[]) new List[5];
Object[] objArray = unsafeArray;
// 往数组里放List<Integer>,运行时数组只能检查到List,无法检查泛型,导致类型污染
objArray[0] = new ArrayList<Integer>();
// 取出时会抛出ClassCastException(运行时异常)
List<String> list = unsafeArray[0];

总结

Java泛型是编译时语法糖,编译完成后会擦除所有类型参数和实参信息,仅保留原始类型。理解“泛型是编译时行为”这一核心,是掌握泛型各类特性的关键。

Java泛型不支持协变,即容器内元素的类型继承关系,与容器本身的类型继承关系相互独立、无关联。


Java-Generics-PartII
http://dracoyus.github.io/2026/01/16/Java-Generics-PartII/
作者
DracoYu
发布于
2026年1月16日
许可协议