Java 泛型与类型擦除:从 PECS 原则到 TypeToken 绕过

Java 泛型与类型擦除:从 PECS 原则到 TypeToken 绕过

写在前面 :泛型是 Java 5 引入的重要特性,它让集合有了类型安全的保证。但 Java 的泛型是"伪泛型"------编译后类型信息会被擦除。List<String> 和 List<Integer> 在运行时其实是同一个类型 List。本文系统讲解泛型擦除机制、通配符 <?> 与 PECS 原则、泛型方法约束,以及用 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。

相关推荐
鶴哥只手遮天1 小时前
Spring Framework 7.0.9 深度剖析-03-IoC 容器核心:BeanFactory 与 BeanDefinition 体系
java·后端·开源
ydjs678899-bot1 小时前
如何在Java中打印pdf静默打印和打印对话框选择
java·python·pdf
Shaoshing1 小时前
存储引擎与架构
java·开发语言·架构
鶴哥只手遮天1 小时前
Spring Framework 7.0.9 深度剖析-01-整体架构与设计哲学
java·后端·开源
const_qiu1 小时前
第 2.5 章 2.5.6:前端登录、Token 无感续签与路由守卫
java·前端·人工智能·状态模式
额额额对了1 小时前
Linux 驱动开发:基于 platform 总线 + misc 设备的 DS18B20 单总线温度驱动
linux·c语言·开发语言·驱动开发·嵌入式硬件
devilnumber1 小时前
Java 代理模式(简单理解回顾)
java·开发语言·代理模式
她的男孩1 小时前
Spring Boot 4 升级别只看编译绿:Forge Admin 在 Jackson、Tomcat JSON 限流、MP 包名与 Flowable 库上踩过的坑
java·spring boot·后端
Knight_AL1 小时前
别把 ThreadLocal 写进 Jackson Serializer
java·spring boot·spring