# 泛型擦除与通配符的边界
作者:Ethan.Yang
博客:https://blog.ethanyang.cn (opens new window)
Java 的泛型机制在语法层面提供了 类型安全 的约束,但由于其 类型擦除特性,也带来了语义不一致、语法限制以及运行时行为差异的问题。
理解这些边界和限制,对高级研发尤为关键,尤其在涉及 泛型反射、框架封装、反序列化与泛型推断 等场景中更显重要。
# 一、类型擦除机制
Java 泛型只在编译期有效,一旦编译完成,泛型信息将被类型擦除(Type Erasure),在运行时不再保留。擦除的规则如下:
- 如果没有限定上界,泛型类型将被擦除为
Object - 如果指定了上界(如
T extends Number),则会擦除为Number - 所有泛型方法或类在运行时都“长得一样”
List<String> list1 = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();
// 运行时擦出了泛型的类型变为Object
System.out.println(list1.getClass() == list2.getClass()); // true
2
3
4
输出 true,说明
List<String>和List<Integer>实际在运行时就是ArrayList,类型信息已丢失。
关注点
- 泛型类型无法用于
instanceof:if (obj instanceof List<String>)是非法的。 - 泛型类型参数不能用于静态上下文:
static T t;会编译失败。 - 泛型类在继承时,如果未显式指定类型参数,容易造成子类代码歧义或泛型丢失。
# 二、泛型数组的限制
泛型数组无法创建的本质原因是:数组具有运行时类型信息和类型检查,而泛型在运行时被擦除,类型不明,检查失效。
List<String>[] array = new ArrayList<String>[10]; // 编译错误
数组是协变的,允许 Object[] = new String[10],但泛型是不可变的,因此 List<Object> ≠ List<String>。将两者混用会破坏类型安全。
合法但有风险的写法:
List<?>[] arr = new List<?>[10]; // 编译通过
arr[0] = new ArrayList<Integer>(); // 编译通过,但类型不安全
2
在高级框架设计中,通常推荐用集合代替泛型数组,或者结合
Class<T>构造对象。
# 三、通配符与 PECS 原则
PECS 原则(Producer Extends, Consumer Super)
? extends T:只读(读取数据,不能写)? super T:只写(写入数据,读取时只能作为 Object)
public void read(List<? extends Number> list) {
Number n = list.get(0); // OK
// list.add(1); // 编译错误
}
public void write(List<? super Integer> list) {
list.add(1); // OK
// Integer i = list.get(0); // 编译错误
}
2
3
4
5
6
7
8
9
关注点
- 在 API 设计中,
extends适用于只读数据源,比如缓存、消息队列等。 super适合用于接收者容器,例如事件处理器、聚合器、持久化器等。
实战经验建议:对外暴露的泛型接口参数,一律使用通配符封装,确保灵活性与兼容性。
# 四、反射中的泛型处理
由于类型擦除,Java 反射无法直接获取泛型参数的实际类型。
class Box<T> {
T value;
}
Field f = Box.class.getDeclaredField("value");
System.out.println(f.getGenericType()); // 输出 T
2
3
4
5
绕过方式:使用匿名子类保留泛型信息
abstract class TypeReference<T> {
private final Type type = ((ParameterizedType)
getClass().getGenericSuperclass()).getActualTypeArguments()[0];
public Type getType() { return type; }
}
TypeReference<List<String>> ref = new TypeReference<List<String>>() {};
System.out.println(ref.getType()); // 输出 java.util.List<java.lang.String>
2
3
4
5
6
7
8
该技巧是 Jackson、Fastjson、Gson 等 JSON 框架常用的泛型保留方式。
关注点
- 利用
ParameterizedType提取泛型信息需搭配匿名子类技巧,否则类型擦除照样生效。 - 若配合
Class<T>一起传递,可以保留部分运行时类型信息,用于工厂、序列化、类型映射等场景。
# 五、泛型方法与类型捕获
# 泛型方法
public class Util {
public static <T> void print(T item) {
System.out.println(item);
}
}
2
3
4
5
相比泛型类,泛型方法更灵活,避免为临时泛型定义额外类型。
# 泛型限定
class A<T extends Number> { } // 上界
// 下界不能用于声明泛型类,只能用于方法参数
public <T> void addAll(List<? super T> list, T value) {
list.add(value);
}
2
3
4
5
6
# 通配符捕获(Wildcard Capture)
public static void process(List<?> list) {
capture(list);
}
private static <T> void capture(List<T> list) {
T element = list.get(0);
System.out.println(element);
}
2
3
4
5
6
7
8
通过泛型方法“捕获”通配符的实际类型,是绕开泛型方法调用限制的技巧之一,适用于工具类设计与泛型转换。
泛型擦除与通配符的边界
← 自动装箱与拆箱 接口默认方法与私有方法机制 →