# 泛型擦除与通配符的边界

作者: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
1
2
3
4

输出 true,说明 List<String>List<Integer> 实际在运行时就是 ArrayList,类型信息已丢失。

关注点

  • 泛型类型无法用于 instanceofif (obj instanceof List<String>) 是非法的。
  • 泛型类型参数不能用于静态上下文:static T t; 会编译失败。
  • 泛型类在继承时,如果未显式指定类型参数,容易造成子类代码歧义或泛型丢失。

# 二、泛型数组的限制

泛型数组无法创建的本质原因是:数组具有运行时类型信息和类型检查,而泛型在运行时被擦除,类型不明,检查失效

List<String>[] array = new ArrayList<String>[10]; // 编译错误
1

数组是协变的,允许 Object[] = new String[10],但泛型是不可变的,因此 List<Object>List<String>。将两者混用会破坏类型安全。

合法但有风险的写法:

List<?>[] arr = new List<?>[10]; // 编译通过
arr[0] = new ArrayList<Integer>(); // 编译通过,但类型不安全
1
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); // 编译错误
}
1
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
1
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>
1
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);
    }
}
1
2
3
4
5

相比泛型类,泛型方法更灵活,避免为临时泛型定义额外类型。

# 泛型限定

class A<T extends Number> { } // 上界

// 下界不能用于声明泛型类,只能用于方法参数
public <T> void addAll(List<? super T> list, T value) {
    list.add(value);
}
1
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);
}
1
2
3
4
5
6
7
8

通过泛型方法“捕获”通配符的实际类型,是绕开泛型方法调用限制的技巧之一,适用于工具类设计与泛型转换。

泛型擦除与通配符的边界