Java 泛型与类型擦除:从 PECS 原则到 TypeToken 绕过
写在前面 :泛型是 Java 5 引入的重要特性,它让集合有了类型安全的保证。但 Java 的泛型是"伪泛型"------编译后类型信息会被擦除。
List<String>和List<Integer>在运行时其实是同一个类型List。本文系统讲解泛型擦除机制、通配符<?>与 PECS 原则、泛型方法约束,以及用 TypeToken/反射参数绕过类型擦除获取真实类型。
目录
- 一、为什么需要泛型
- 二、泛型的基本使用
- 三、类型擦除机制
- [四、通配符与 PECS 原则](#四、通配符与 PECS 原则)
- 五、泛型方法
- [六、绕过类型擦除:TypeToken 与反射参数](#六、绕过类型擦除:TypeToken 与反射参数)
- 七、实战避坑清单
- 八、总结
一、为什么需要泛型
1.1 泛型之前的痛点
java
// Java 5 之前:集合只能存 Object
List list = new ArrayList();
list.add("hello");
list.add(123); // 编译通过!任何类型都能塞进去
String s = (String) list.get(1); // 运行时 ClassCastException!
// 类型不安全,编译期不报错,运行时崩溃
1.2 泛型带来的好处
java
// 有泛型后:编译期类型检查
List<String> list = new ArrayList<>();
list.add("hello");
// list.add(123); // 编译错误!只能存 String ← 编译期就拦住了
String s = list.get(0); // 无需强转
| 好处 | 说明 |
|---|---|
| 类型安全 | 编译期检查,减少运行时 ClassCastException |
| 消除强转 | 取值时编译器自动插入强转代码 |
| 代码复用 | 一套代码适用于多种类型 |
二、泛型的基本使用
2.1 泛型类
java
// 自定义泛型类
public class Box<T> {
private T value;
public Box(T value) { this.value = value; }
public T getValue() { return value; }
public void setValue(T value) { this.value = value; }
}
// 使用
Box<String> stringBox = new Box<>("Hello");
String s = stringBox.getValue(); // 无需强转
Box<Integer> intBox = new Box<>(42);
Integer i = intBox.getValue();
2.2 多类型参数
java
public class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() { return key; }
public V getValue() { return value; }
}
// 使用
Pair<String, Integer> p = new Pair<>("age", 25);
String key = p.getKey();
Integer value = p.getValue();
2.3 泛型接口
java
public interface Comparable<T> {
int compareTo(T o);
}
// 实现泛型接口
public class Product implements Comparable<Product> {
private String name;
private double price;
@Override
public int compareTo(Product other) {
return Double.compare(this.price, other.price);
}
}
三、类型擦除机制
3.1 什么是类型擦除
Java 的泛型是通过类型擦除(Type Erasure)实现的 :编译器在编译时检查泛型类型,然后在生成的字节码中擦除所有泛型类型信息,替换为原始类型(Raw Type)或第一个上界。
⭐ 核心代码 --- 类型擦除前后对比
java
// 源码
public class Box<T> {
private T value;
public T getValue() { return value; }
public void setValue(T value) { this.value = value; }
}
// 编译后(类型擦除后的等价形式)
public class Box {
private Object value; // T 被擦除为 Object(上界)
public Object getValue() { return value; }
public void setValue(Object value) { this.value = value; }
}
// 调用处编译器自动插入强转
Box<String> box = new Box<>();
box.setValue("hello");
String s = box.getValue();
// 编译后等价于:
// String s = (String) box.getValue(); ← 编译器插入强转
3.2 运行时证明类型被擦除
java
public class ErasureProof {
public static void main(String[] args) {
List<String> stringList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
// 运行时类型相同!泛型信息已被擦除
System.out.println(stringList.getClass() == intList.getClass()); // true
System.out.println(stringList.getClass().getName()); // java.util.ArrayList
// 没有 <String> 信息
// 无法用 instanceof 检查泛型类型
// if (stringList instanceof List<String>) { } // 编译错误!
if (stringList instanceof List) { } // 只能检查原始类型
}
}
3.3 有上界的类型擦除
java
// T 的上界是 Number
public class NumberBox<T extends Number> {
private T value;
public T getValue() { return value; }
}
// 擦除后:T 替换为第一个上界 Number,而不是 Object
public class NumberBox {
private Number value;
public Number getValue() { return value; }
}
3.4 类型擦除带来的限制
| 限制 | 原因 | 示例 |
|---|---|---|
不能 new T() |
运行时不知道 T 是什么 | new T() ❌ |
| 不能创建泛型数组 | 数组有协变性,泛型没有 | new T[10] ❌ |
| 不能用基本类型做泛型参数 | 泛型擦除为 Object | List<int> ❌ |
不能 instanceof List<String> |
运行时没有泛型信息 | ❌ |
| 静态字段不能使用类的泛型参数 | 泛型参数属于实例 | static T value ❌ |
| 重载方法签名冲突 | 擦除后参数类型相同 | void m(List<String>) 和 void m(List<Integer>) 冲突 ❌ |
java
public class GenericLimitations<T> {
// ❌ 不能 new T()
// T item = new T();
// ❌ 不能创建泛型数组
// T[] array = new T[10];
// ❌ 静态字段不能使用 T
// private static T staticValue;
// ✅ 可通过 Class 反射创建
private Class<T> type;
public GenericLimitations(Class<T> type) { this.type = type; }
public T newInstance() throws Exception {
return type.getDeclaredConstructor().newInstance();
}
}
四、通配符与 PECS 原则
4.1 三种通配符
| 通配符 | 含义 | 说明 |
|---|---|---|
<?> |
无界通配符 | 接受任何类型 |
<? extends T> |
上界通配符 | T 或 T 的子类 |
<? super T> |
下界通配符 | T 或 T 的父类 |
4.2 <? extends T> 上界通配符
java
// Number 的子类包括 Integer, Double, Long 等
List<? extends Number> list = new ArrayList<Integer>();
list = new ArrayList<Double>();
list = new ArrayList<Long>();
Number n = list.get(0); // ✅ 可以读(读出来是 Number)
// list.add(42); // ❌ 不能写!编译器不知道具体类型是什么
// 编译器:可能是 List<Double>,往里面放 Integer 就错了
4.3 <? super T> 下界通配符
java
List<? super Integer> list = new ArrayList<Integer>();
list = new ArrayList<Number>();
list = new ArrayList<Object>();
list.add(42); // ✅ 可以写(Integer 一定是 list 声明类型的子类)
// Integer n = list.get(0); // ❌ 不能读(取出来可能是 Object)
Object o = list.get(0); // ✅ 只能用 Object 接收
4.4 ⭐ PECS 原则
⭐ 核心原则 --- PECS(Producer Extends, Consumer Super)
如果你需要从集合中读取数据(生产者)→ 用 <? extends T>
如果你需要往集合中写入数据(消费者)→ 用 <? super T>
如果既读又写 → 不用通配符,直接用 <T>
⭐ 核心代码 --- PECS 实战:拷贝方法
java
public class Collections {
// JDK 源码中的 copy 方法签名
// src 是生产者(提供数据)→ extends
// dest 是消费者(接收数据)→ super
public static <T> void copy(
List<? super T> dest, // 消费者 → super
List<? extends T> src // 生产者 → extends
) {
for (int i = 0; i < src.size(); i++) {
dest.set(i, src.get(i)); // 从 src 读,写入 dest
}
}
}
// 使用
List<Integer> src = Arrays.asList(1, 2, 3);
List<Number> dest = new ArrayList<>(Arrays.asList(0, 0, 0));
Collections.copy(dest, src); // 把 Integer 拷贝到 Number 列表中
// dest 是 Number 列表(Integer 的父类)→ super
// src 是 Integer 列表(Number 的子类)→ extends
4.5 实战示例:通配符在设计中的应用
java
// 场景:动物喂食系统
class Animal { void eat() {} }
class Dog extends Animal { void bark() {} }
class Cat extends Animal { void meow() {} }
// ✅ 使用 extends --- 从动物列表中读取(生产者)
public void feedAnimals(List<? extends Animal> animals) {
for (Animal a : animals) { // 读 → extends
a.eat();
}
}
// ✅ 使用 super --- 往列表中添加(消费者)
public void addDogs(List<? super Dog> dogs) {
dogs.add(new Dog()); // 写 → super
dogs.add(new Dog());
}
// 使用
List<Dog> dogList = new ArrayList<>();
addDogs(dogList); // List<Dog> 符合 <? super Dog>
List<Animal> animalList = new ArrayList<>();
addDogs(animalList); // List<Animal> 也符合 <? super Dog>
五、泛型方法
5.1 基本语法
⭐ 核心代码 --- 泛型方法定义
java
public class GenericMethod {
// 泛型方法:<T> 声明类型参数,T 是返回类型
public static <T> T getFirst(List<T> list) {
if (list == null || list.isEmpty()) return null;
return list.get(0);
}
// 多类型参数
public static <K, V> Pair<K, V> makePair(K key, V value) {
return new Pair<>(key, value);
}
// 有上界约束
public static <T extends Comparable<T>> T max(T a, T b) {
return (a.compareTo(b) >= 0) ? a : b;
}
}
5.2 泛型方法 vs 泛型类
java
// 泛型类 --- T 在类级别声明,整个类共用
public class Box<T> {
public T getValue() { ... } // T 来自类声明
}
// 泛型方法 --- T 在方法级别声明,独立于类
public class Utils {
public static <T> T identity(T value) { // T 来自方法声明
return value;
}
}
// 泛型类中的泛型方法可以有不同的类型参数
public class Container<T> {
private T content;
// 方法的 R 与类的 T 无关
public <R> R transform(Function<T, R> mapper) {
return mapper.apply(content);
}
}
5.3 泛型方法的类型推断
java
// Java 7 之前需要显式指定类型参数
Collections.<String>sort(list, comparator);
// Java 8+ 自动推断
Collections.sort(list, comparator); // 编译器从 list 推断 <String>
// 钻石操作符
List<String> list = new ArrayList<>(); // <> 让编译器推断
// Java 11+ 可以在匿名类使用 var + <>
var list = new ArrayList<>(List.of("a", "b"));
六、绕过类型擦除:TypeToken 与反射参数
6.1 通过 Class 参数传递类型
java
public class GenericRepository<T> {
private Class<T> type;
// 通过构造函数传入 Class 对象
public GenericRepository(Class<T> type) {
this.type = type;
}
// 可以在运行时获取真实类型
public T findById(Long id) {
// 模拟从数据库查询
// return entityManager.find(type, id);
System.out.println("查询 " + type.getSimpleName() + ", id=" + id);
try {
return type.getDeclaredConstructor().newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public Class<T> getType() {
return type;
}
}
// 使用
GenericRepository<User> repo = new GenericRepository<>(User.class);
// 运行时知道 type 是 User.class
User user = repo.findById(1L);
System.out.println(repo.getType().getSimpleName()); // User
6.2 Gson 的 TypeToken 方案
Gson 需要在运行时知道泛型的真实类型来反序列化 JSON,它通过创建匿名子类来保留泛型信息:
⭐ 核心代码 --- TypeToken 原理
java
// Gson 的 TypeToken 利用匿名类的父类泛型参数在字节码中保留的原理
public abstract class TypeToken<T> {
private final Type type;
protected TypeToken() {
// 获取父类的泛型参数
Type superClass = getClass().getGenericSuperclass();
if (superClass instanceof ParameterizedType) {
ParameterizedType pt = (ParameterizedType) superClass;
type = pt.getActualTypeArguments()[0];
} else {
throw new RuntimeException("缺少类型参数");
}
}
public Type getType() { return type; }
}
// 使用 --- 创建匿名子类
TypeToken<List<String>> token = new TypeToken<List<String>>() {};
// 匿名类的父类是 TypeToken<List<String>>
// 在字节码中,父类的泛型参数是保留的
Type type = token.getType();
System.out.println(type); // java.util.List<java.lang.String>
// 成功在运行时获取了泛型的真实类型!
6.3 TypeToken 原理详解
为什么匿名子类能保留泛型信息?
java
// 普通泛型类 --- 编译后擦除
class MyClass<T> { }
// MyClass<String> 和 MyClass<Integer> 编译后都是 MyClass
// 匿名子类 --- 父类泛型参数保留在字节码中
new TypeToken<List<String>>() {}
// 编译后的匿名类继承 TypeToken<List<String>>
// 字节码中的 Signature 属性记录了父类的泛型参数
// 反射时 getGenericSuperclass() 可以读出来
⭐ 核心代码 --- 验证匿名子类的泛型信息
java
import java.lang.reflect.*;
public class TypeTokenProof {
public static void main(String[] args) {
// 匿名子类
TypeToken<List<String>> token = new TypeToken<List<String>>() {};
// 获取父类的泛型类型
Type superType = token.getClass().getGenericSuperclass();
System.out.println(superType);
// 输出: TypeToken<java.util.List<java.lang.String>>
if (superType instanceof ParameterizedType) {
ParameterizedType pt = (ParameterizedType) superType;
Type[] args2 = pt.getActualTypeArguments();
System.out.println(args2[0]);
// 输出: java.util.List<java.lang.String>
}
}
}
七、实战避坑清单
| 序号 | 陷阱 | 正确做法 |
|---|---|---|
| 1 | 运行时获取泛型类型 | 传 Class<T> 参数或用 TypeToken |
| 2 | List<Integer> 不能赋给 List<Number> |
泛型不协变,用 List<? extends Number> |
| 3 | 往 List<? extends T> 中添加元素 |
extends 只能读,写用 <? super T> |
| 4 | 从 List<? super T> 中读取具体类型 |
super 只能写,读只能用 Object 接收 |
| 5 | 泛型方法忘了 <T> 声明 |
方法返回类型前必须有 <T> |
| 6 | 泛型用基本类型 | 用包装类 List<Integer> 而非 List<int> |
| 7 | 创建泛型数组 | 用 List<T> 或 Object[] + 强转 |
| 8 | 泛型类继承时类型参数冲突 | 子类必须明确指定或继续传递 |
7.1 泛型不协变的问题
java
// Integer 是 Number 的子类
// 但 List<Integer> 不是 List<Number> 的子类!
List<Integer> intList = new ArrayList<>();
// List<Number> numList = intList; // 编译错误!
// 原因:如果允许,下面的代码就能编译但运行时崩溃
// numList.add(3.14); // 往 Integer 列表里放 Double!
// 用通配符解决
List<? extends Number> numList = intList; // ✅ 编译通过
// numList.add(3.14); // ❌ 仍然不能写,安全!
7.2 数组协变 vs 泛型不协变
java
// 数组是协变的(有缺陷)
Integer[] intArray = new Integer[10];
Number[] numArray = intArray; // ✅ 编译通过
numArray[0] = 3.14; // 运行时 ArrayStoreException!
// 泛型不协变(更安全)
List<Integer> intList = new ArrayList<>();
// List<Number> numList = intList; // 编译错误! ← 编译期就拦住了
💡 Java 选择"泛型不协变 + 通配符"的设计,比"数组协变"更安全。
八、总结
Java 泛型知识图谱:
基本使用
├── 泛型类: class Box<T> {}
├── 泛型接口: interface Comparable<T> {}
├── 多参数: class Pair<K, V> {}
└── 泛型方法: public static <T> T getFirst(List<T> list)
类型擦除
├── 编译时检查 → 运行时擦除
├── 无界 → 擦除为 Object
├── 有界 → 擦除为上界
├── 限制: 不能 new T / 泛型数组 / instanceof
└── 运行时 List<String> == List<Integer>
通配符 & PECS
├── <? extends T> → 只读(生产者)
├── <? super T> → 只写(消费者)
├── <?> → 只读 Object
├── PECS: Producer Extends, Consumer Super
└── 泛型不协变: List<Integer> ≠ List<Number>
绕过擦除
├── 传 Class<T> 参数 → 运行时知道类型
├── TypeToken 匿名子类 → 保留父类泛型
│ ├── getGenericSuperclass()
│ └── getActualTypeArguments()
└── Gson/Jackson 用此机制反序列化泛型
一句话记住核心:
Java 泛型是编译期擦除的伪泛型,读取用
<? extends T>,写入用<? super T>,运行时需要真实类型就传Class<T>或用 TypeToken。