编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

  • Cpp入门到精通

  • Java入门精通

    • README
    • 入门教程

    • 综合案例

    • 专栏博客

      • README
      • JVM内存模型与对象
      • 类加载与双亲委派
      • 垃圾回收与GC调优
      • 异常体系与JVM机制
      • 字节码指令集javap实战
      • JIT编译与去优化机制
      • JVM性能诊断工具链
      • OOM八大现场全景剖析
      • JVM参数调优全景图
      • GraalVM与AOT编译原理
      • HashMap底层哈希设计
      • String不可变与常量池
      • ArrayList与LinkedList源码
      • ConcurrentHashMap并发
      • TreeMap与红黑树原理
      • LinkedHashMap与LRU实现
      • Java数字类型原理
      • Object通用方法的契约
      • 泛型擦除与类型系统
        • 6.1 开篇疑问
        • 6.2 泛型的诞生背景
          • 6.2.1 没有泛型的黑暗时代
          • 6.2.2 擦除式泛型vs具化式泛型
          • 6.2.3 Java为什么选择类型擦除
        • 6.3 泛型擦除的本质
          • 6.3.1 什么是类型擦除
          • 6.3.2 擦除后的真实面目
          • 6.3.3 编译器插入的强制转换
          • 6.3.4 桥接方法的秘密
          • 6.3.5 用javap验证擦除过程
        • 6.4 泛型的边界与限制
          • 6.4.1 泛型不能用基本类型
          • 6.4.2 不能创建泛型数组
          • 6.4.3 不能instanceof泛型
          • 6.4.4 不能new T和new T数组
          • 6.4.5 泛型类的静态上下文限制
          • 6.4.6 泛型异常的限制
        • 6.5 通配符与PECS原则
          • 6.5.1 为什么需要通配符
          • 6.5.2 上界通配符extends详解
          • 6.5.3 下界通配符super详解
          • 6.5.4 PECS原则详解与实战
          • 6.5.5 无界通配符的场景
        • 6.6 泛型的高级特性
          • 6.6.1 泛型方法与类型推断
          • 6.6.2 递归类型边界
          • 6.6.3 多重边界
          • 6.6.4 泛型与可变参数
        • 6.7 运行时获取泛型信息
          • 6.7.1 Signature属性保留泛型
          • 6.7.2 反射获取泛型参数
          • 6.7.3 TypeToken模式
          • 6.7.4 框架中的泛型获取
        • 6.8 泛型最佳实践与常见陷阱
          • 6.8.1 泛型方法优于泛型类
          • 6.8.2 常见的泛型陷阱
          • 6.8.3 类型安全的异构容器
        • 6.9 Java vs 其他语言的泛型对比
        • 6.10 总结与核心要点
      • 枚举原理与最佳实践
      • 注解原理与编译期处理
      • Lambda与引用底层原理
      • Stream原理与流水线设计
      • Optional设计原理
      • Record密封类与模式
      • 反射机制与动态代理
      • MethodHandle与VarHandle
      • 三大字节码框架对比
      • JavaAgent与Instrumentation机制
      • AOP三种实现路线对比
      • synchronized与锁升级
      • volatile与JMM内存模型
      • 线程池核心源码设计
      • Thread线程生命周期
      • AQS同步框架源码
      • 并发锁三剑客
      • CAS和Atomic深入分析
      • 五大同步器对比
      • CompletableFuture异步
      • IO模型演进BIO到AIO
      • ByteBuffer与堆外内存
      • 序列化原理与替代方案
      • 文件IO与NIO.2
      • 面向对象的真意
      • JDK设计模式上
      • JDK设计模式下
      • SPI与模块化设计
  • Go入门到精通

  • JavaScript入门

  • CodeX
  • Java入门精通
  • 专栏博客
杨充
2026-06-02
目录

泛型擦除与类型系统

# 19.泛型擦除与类型系统

# 目录介绍

  • 6.1 开篇疑问
  • 6.2 泛型的诞生背景
    • 6.2.1 没有泛型的黑暗时代
    • 6.2.2 擦除式泛型vs具化式泛型
    • 6.2.3 Java为什么选择类型擦除
  • 6.3 泛型擦除的本质
    • 6.3.1 什么是类型擦除
    • 6.3.2 擦除后的真实面目
    • 6.3.3 编译器插入的强制转换
    • 6.3.4 桥接方法的秘密
    • 6.3.5 用javap验证擦除过程
  • 6.4 泛型的边界与限制
    • 6.4.1 泛型不能用基本类型
    • 6.4.2 不能创建泛型数组
    • 6.4.3 不能instanceof泛型
    • 6.4.4 不能new T和new T数组
    • 6.4.5 泛型类的静态上下文限制
    • 6.4.6 泛型异常的限制
  • 6.5 通配符与PECS原则
    • 6.5.1 为什么需要通配符
    • 6.5.2 上界通配符extends详解
    • 6.5.3 下界通配符super详解
    • 6.5.4 PECS原则详解与实战
    • 6.5.5 无界通配符的场景
  • 6.6 泛型的高级特性
    • 6.6.1 泛型方法与类型推断
    • 6.6.2 递归类型边界
    • 6.6.3 多重边界
    • 6.6.4 泛型与可变参数
  • 6.7 运行时获取泛型信息
    • 6.7.1 Signature属性保留泛型
    • 6.7.2 反射获取泛型参数
    • 6.7.3 TypeToken模式
    • 6.7.4 框架中的泛型获取
  • 6.8 泛型最佳实践与常见陷阱
    • 6.8.1 泛型方法优于泛型类
    • 6.8.2 常见的泛型陷阱
    • 6.8.3 类型安全的异构容器
  • 6.9 Java vs 其他语言的泛型对比
  • 6.10 总结与核心要点

# 6.1 开篇疑问

疑惑:Java 的泛型为什么是"假泛型"?List<Integer> 和 List<String> 在运行时是同一个类型吗?为什么泛型不能用 int 只能用 Integer?为什么不能 new T()?泛型擦除后,框架是怎么获取泛型类型信息的?

答疑:Java 泛型是 JDK 5 引入的,采用了"类型擦除"的实现方式。这个设计让泛型与旧版本代码完全兼容,但也带来了很多反直觉的限制。理解擦除机制,是理解泛型所有"怪异行为"的钥匙。

本篇将从泛型的诞生背景出发,深入分析擦除机制、桥接方法、通配符 PECS 原则,以及框架如何在运行时绕过擦除获取泛型信息。

# 6.2 泛型的诞生背景

# 6.2.1 没有泛型的黑暗时代

JDK 5 之前,集合只能存 Object,取出时必须强转:

// JDK 1.4 时代——类型不安全
List list = new ArrayList();
list.add("hello");
list.add(123);                      // 可以放任何类型,编译器不报错
list.add(new Object());

String s = (String) list.get(0);    // 需要强转
String s2 = (String) list.get(1);   // 运行时 ClassCastException!

// 问题:编译器无法帮你检查类型错误
// 错误延迟到运行时才暴露,排查困难
1
2
3
4
5
6
7
8
9
10
11

泛型的目标:将类型检查从运行时提前到编译时。

// JDK 5+ 泛型——编译期安全
List<String> list = new ArrayList<>();
list.add("hello");
list.add(123);                      // 编译报错!类型不匹配
String s = list.get(0);             // 无需强转,编译器保证类型正确
1
2
3
4
5

# 6.2.2 擦除式泛型vs具化式泛型

实现泛型有两种方式:

特性 擦除式(Java) 具化式(C#)
运行时类型信息 不保留泛型参数 保留泛型参数
List<int> 支持 不支持(必须用包装类) 支持
new T() 支持 不支持 支持
instanceof List<String> 不支持 支持
运行时每种泛型一个类 否(所有 List<X> 共用一个类) 是(List<int> 和 List<string> 是不同的类)
向后兼容 完全兼容旧代码 不兼容

# 6.2.3 Java为什么选择类型擦除

核心原因:向后兼容。

JDK 5 发布时,已有海量基于 JDK 1.4 的代码和库。Java 必须保证:

  1. 旧代码不修改就能在新 JVM 上运行
  2. 新代码能调用旧的非泛型库
  3. 旧代码能调用新的泛型库
// 旧代码(JDK 1.4 编写)
public void process(List list) {
    // ...
}

// 新代码(JDK 5 编写)
List<String> strings = new ArrayList<>();
strings.add("hello");

// 旧代码可以直接使用新的泛型 List(向后兼容)
process(strings);  // 合法,泛型擦除后 List<String> 就是 List
1
2
3
4
5
6
7
8
9
10
11

C# 之所以能用具化式泛型,是因为 .NET 2.0 引入泛型时同时修改了 CLR(虚拟机),不需要与旧版本兼容。Java 选择不修改 JVM 字节码格式,只在编译器层面实现泛型。

代价:擦除带来了各种限制(不能 new T()、不能 instanceof 等),这些"怪异行为"都是向后兼容的代价。

# 6.3 泛型擦除的本质

# 6.3.1 什么是类型擦除

编译时泛型信息用于类型检查,编译完成后泛型信息被擦除,字节码中不包含泛型参数。

// 源代码
List<String> stringList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();

// 编译后(字节码层面)——完全相同
List stringList = new ArrayList();
List intList = new ArrayList();

// 验证
System.out.println(stringList.getClass() == intList.getClass()); // true
System.out.println(stringList.getClass().getName()); // java.util.ArrayList
// 运行时根本不知道 stringList 是 List<String>
1
2
3
4
5
6
7
8
9
10
11
12

# 6.3.2 擦除后的真实面目

泛型参数被替换为其上界(没有上界则替换为 Object):

// 无界泛型:T → Object
public class Box<T> {
    private T value;
    public T get() { return value; }
    public void set(T value) { this.value = value; }
}
// 擦除后
public class Box {
    private Object value;
    public Object get() { return value; }
    public void set(Object value) { this.value = value; }
}

// 有上界泛型:T extends Number → Number
public class NumberBox<T extends Number> {
    private T value;
    public double doubleValue() { return value.doubleValue(); }
}
// 擦除后
public class NumberBox {
    private Number value;
    public double doubleValue() { return value.doubleValue(); }
}

// 多重上界:T extends Comparable & Serializable → 第一个边界 Comparable
public class MultiBox<T extends Comparable<T> & Serializable> {
    private T value;
}
// 擦除后
public class MultiBox {
    private Comparable value;  // 使用第一个边界
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

关键点:有多个边界时,擦除为第一个边界。因此建议将"标签接口"(如 Serializable)放在后面,将实际使用方法的接口放在前面,以减少强制转换。

# 6.3.3 编译器插入的强制转换

编译器在调用处自动插入 checkcast 指令:

// 源代码
Box<String> box = new Box<>();
box.set("hello");
String s = box.get();

// 编译后的字节码等价于
Box box = new Box();
box.set("hello");
String s = (String) box.get();   // 编译器自动插入 checkcast
1
2
3
4
5
6
7
8
9

字节码验证:

invokevirtual Box.get:()Ljava/lang/Object;   // 返回 Object
checkcast     java/lang/String                // 强转为 String
astore_2
1
2
3

这就是泛型的本质:编译器在编译期做类型检查,在使用处插入强制转换,然后擦除泛型信息。类型安全由编译器保证,运行时的 checkcast 只是"双保险"。

# 6.3.4 桥接方法的秘密

类型擦除可能导致方法签名冲突,编译器通过桥接方法(Bridge Method)解决多态问题:

public interface Comparable<T> {
    int compareTo(T o);
}

public class IntValue implements Comparable<Integer> {
    private int value;
    
    @Override
    public int compareTo(Integer other) {  // 参数是 Integer
        return Integer.compare(this.value, other);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

问题:擦除后 Comparable 接口的方法是 compareTo(Object),但 IntValue 实现的是 compareTo(Integer)。签名不匹配,多态会失效!

解决:编译器自动生成桥接方法:

// 编译器在 IntValue 中自动生成
public class IntValue implements Comparable {
    // 用户编写的方法
    public int compareTo(Integer other) {
        return Integer.compare(this.value, other);
    }
    
    // 编译器生成的桥接方法(字节码中可见)
    public int compareTo(Object other) {       // bridge method
        return this.compareTo((Integer) other); // 转发给真正的方法
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

验证桥接方法:

for (Method m : IntValue.class.getDeclaredMethods()) {
    System.out.println(m.getName() 
        + " bridge=" + m.isBridge() 
        + " synthetic=" + m.isSynthetic()
        + " params=" + Arrays.toString(m.getParameterTypes()));
}
// compareTo bridge=false synthetic=false params=[class java.lang.Integer]
// compareTo bridge=true  synthetic=true  params=[class java.lang.Object]
1
2
3
4
5
6
7
8

# 6.3.5 用javap验证擦除过程

# 编译并查看字节码
javac Box.java
javap -c -s -p Box.class
1
2
3
public class Box {
  private java.lang.Object value;
    descriptor: Ljava/lang/Object;

  public java.lang.Object get();
    descriptor: ()Ljava/lang/Object;
    Code:
       0: aload_0
       1: getfield      #2  // Field value:Ljava/lang/Object;
       4: areturn

  // Signature 属性中保留了泛型信息(不在字节码指令中,而在元数据中)
  // Signature: Ljava/lang/Object;  →  实际为 TT;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 6.4 泛型的边界与限制

# 6.4.1 泛型不能用基本类型

List<int> list = new ArrayList<>();       // 编译错误!
List<Integer> list = new ArrayList<>();   // 正确,使用包装类
1
2

原因:擦除后 T 变成 Object,而 int 不是 Object 的子类。Java 的基本类型不参与面向对象体系。

// 擦除后 List<T> 的 add 方法变成:
public boolean add(Object e) { ... }

// int 不能赋值给 Object(没有自动装箱发生在泛型层面)
// 虽然 Java 5 引入了自动装箱,但那是在使用处发生的
1
2
3
4
5

性能影响:使用包装类意味着装箱/拆箱的开销。处理大量数值时,考虑使用专门的原始类型集合库(如 Eclipse Collections 的 IntList)。

未来展望:Project Valhalla 正在为 Java 引入值类型和基本类型泛型(Primitive Generics),预计未来版本支持 List<int>。

# 6.4.2 不能创建泛型数组

// 编译错误
T[] arr = new T[10];
List<String>[] arr = new List<String>[10];

// 编译警告(不安全)
List<String>[] arr = new List[10];
1
2
3
4
5
6

疑惑:为什么数组可以协变但泛型不行?

论证:Java 数组是协变的(String[] 是 Object[] 的子类型),且运行时知道元素类型:

Object[] arr = new String[3];
arr[0] = "hello";     // OK
arr[1] = 123;         // 运行时 ArrayStoreException!数组做了类型检查

// 如果允许泛型数组:
List<String>[] arr = new List<String>[3];  // 假设合法
Object[] objArr = arr;                      // 数组协变
objArr[0] = new ArrayList<Integer>();       // 运行时无法检查!
// 因为擦除后 List<String> 和 List<Integer> 都是 List
// 类型安全被破坏!
1
2
3
4
5
6
7
8
9
10

替代方案:

// 使用 ArrayList 代替数组
List<List<String>> listOfLists = new ArrayList<>();

// 使用 @SuppressWarnings(确认安全时)
@SuppressWarnings("unchecked")
T[] arr = (T[]) new Object[10];

// 使用 Array.newInstance
@SuppressWarnings("unchecked")
T[] arr = (T[]) Array.newInstance(clazz, size);
1
2
3
4
5
6
7
8
9
10

# 6.4.3 不能instanceof泛型

if (obj instanceof List<String>) { }   // 编译错误!
if (obj instanceof List<?>) { }        // 可以(无界通配符不受擦除影响)
if (obj instanceof List) { }           // 可以(原始类型)
1
2
3

原因:instanceof 是运行时检查,而泛型信息在运行时已被擦除。JVM 无法区分 List<String> 和 List<Integer>。

# 6.4.4 不能new T和new T数组

public class Factory<T> {
    public T create() {
        return new T();        // 编译错误!
        // 擦除后变成 new Object(),不是用户期望的类型
    }
}
1
2
3
4
5
6

替代方案一:传入 Class 对象

public class Factory<T> {
    private Class<T> clazz;
    
    public Factory(Class<T> clazz) {
        this.clazz = clazz;
    }
    
    public T create() throws Exception {
        return clazz.getDeclaredConstructor().newInstance();
    }
}

// 使用
Factory<User> factory = new Factory<>(User.class);
User user = factory.create();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

替代方案二:传入 Supplier

public class Factory<T> {
    private Supplier<T> supplier;
    
    public Factory(Supplier<T> supplier) {
        this.supplier = supplier;
    }
    
    public T create() {
        return supplier.get();
    }
}

// 使用
Factory<User> factory = new Factory<>(User::new);
User user = factory.create();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 6.4.5 泛型类的静态上下文限制

public class Box<T> {
    // 静态字段不能用类的泛型参数
    private static T value;              // 编译错误!
    
    // 静态方法不能用类的泛型参数
    public static T getValue() { }       // 编译错误!
    
    // 但静态方法可以定义自己的泛型参数
    public static <E> E convert(E input) { return input; }  // OK
}
1
2
3
4
5
6
7
8
9
10

原因:泛型参数属于实例级别。Box<String> 和 Box<Integer> 共享同一个 Class 对象和静态字段。如果静态字段是 T 类型,它到底是 String 还是 Integer?无法确定。

# 6.4.6 泛型异常的限制

// 不能声明泛型异常类
public class GenericException<T> extends Exception { }  // 编译错误!

// 不能 catch 泛型异常
try { ... }
catch (T e) { }         // 编译错误!

// 但可以在 throws 中使用类型参数
public <T extends Exception> void foo() throws T { }  // OK
1
2
3
4
5
6
7
8
9

# 6.5 通配符与PECS原则

# 6.5.1 为什么需要通配符

核心问题:泛型不是协变的。

// 数组是协变的
Object[] arr = new String[3];   // OK:String[] 是 Object[] 的子类型

// 泛型不是协变的
List<Object> list = new ArrayList<String>();  // 编译错误!
// List<String> 不是 List<Object> 的子类型
1
2
3
4
5
6

疑惑:String 是 Object 的子类,为什么 List<String> 不是 List<Object> 的子类?

论证:如果 List<String> 是 List<Object> 的子类型:

List<String> strings = new ArrayList<>();
List<Object> objects = strings;   // 假设合法
objects.add(123);                 // List<Object> 可以放 Integer
String s = strings.get(0);       // 运行时 ClassCastException!
// 类型安全被破坏
1
2
3
4
5

为了在保持类型安全的同时允许灵活的参数传递,Java 引入了通配符。

# 6.5.2 上界通配符extends详解

<? extends T> 表示"T 或 T 的某个子类",但具体是哪个子类未知。

// 可以接受 List<Number>、List<Integer>、List<Double> 等
public double sum(List<? extends Number> list) {
    double total = 0;
    for (Number n : list) {
        total += n.doubleValue();   // 可以读取为 Number 类型
    }
    return total;
}

// 使用
sum(new ArrayList<Integer>());   // OK
sum(new ArrayList<Double>());    // OK
sum(new ArrayList<String>());    // 编译错误!String 不是 Number 的子类
1
2
3
4
5
6
7
8
9
10
11
12
13

不能写入(除了 null):

List<? extends Number> list = new ArrayList<Integer>();

Number n = list.get(0);       // OK:可以读取为 Number
Integer i = list.get(0);      // 编译错误:可能是 Double

list.add(1);                  // 编译错误!
list.add(1.0);                // 编译错误!
list.add(null);               // OK:null 是任何引用类型的合法值
1
2
3
4
5
6
7
8

为什么不能写入? 编译器不知道 ? 代表哪个具体子类。如果实际是 List<Double>,放入 Integer 就破坏了类型安全。编译器为了安全,干脆禁止所有非 null 的写入。

# 6.5.3 下界通配符super详解

<? super T> 表示"T 或 T 的某个父类"。

List<? super Integer> list = new ArrayList<Number>();

// 可以写入 Integer 及其子类
list.add(1);                    // OK:Integer 是 Integer
list.add((short) 1);            // 编译错误:Short 不是 Integer 的子类

// 读取只能得到 Object
Object obj = list.get(0);      // OK:只知道是 Object
Integer i = list.get(0);       // 编译错误:可能是 Number
1
2
3
4
5
6
7
8
9

为什么可以写入 Integer? 无论 ? 代表 Integer、Number 还是 Object,Integer 都是它们的子类型,写入安全。

为什么读取只能得到 Object? ? 可能是 Integer、Number 或 Object,编译器只能确定它是 Object 的子类。

# 6.5.4 PECS原则详解与实战

Producer Extends, Consumer Super:

角色 通配符 操作 助记
生产者(从中读取) ? extends T 只能读 PE
消费者(向其写入) ? super T 只能写 CS
既读又写 不用通配符 精确类型 —

Collections.copy 的经典设计:

public static <T> void copy(
    List<? super T> dest,       // 消费者:写入 T 类型的数据
    List<? extends T> src       // 生产者:读取 T 类型的数据
) {
    for (int i = 0; i < src.size(); i++) {
        T item = src.get(i);     // 从 extends 中读取
        dest.set(i, item);       // 向 super 中写入
    }
}

// 使用示例
List<Number> numbers = new ArrayList<>(Arrays.asList(0, 0, 0));
List<Integer> integers = Arrays.asList(1, 2, 3);
Collections.copy(numbers, integers);
// dest 是 List<? super Integer>(Number 是 Integer 的父类)
// src  是 List<? extends Integer>(Integer extends Integer)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

实战:设计灵活的 API

// 不灵活的 API
public static double sumList(List<Number> list) { ... }
// 只能传 List<Number>,不能传 List<Integer>

// 灵活的 API(使用 PECS)
public static double sumList(List<? extends Number> list) { ... }
// 可以传 List<Number>、List<Integer>、List<Double>

// Comparable 的正确声明
public static <T extends Comparable<? super T>> T max(List<? extends T> list) {
    // T extends Comparable<? super T>:T 可以与自身或父类比较
    // List<? extends T>:可以传入 T 或 T 的子类的 List
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 6.5.5 无界通配符的场景

<?> 表示"未知类型",等价于 <? extends Object>。

// 只需要调用 Object 的方法时
public static void printAll(List<?> list) {
    for (Object item : list) {
        System.out.println(item);   // Object.toString()
    }
}

// 不关心类型参数时
public static boolean isEmpty(List<?> list) {
    return list == null || list.isEmpty();
}

// Class<?> 比 Class(原始类型)更安全
Class<?> clazz = String.class;  // 表示"某个类",而非"任何类"
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 6.6 泛型的高级特性

# 6.6.1 泛型方法与类型推断

// 泛型方法:类型参数声明在方法级别
public static <T> List<T> singletonList(T item) {
    return Collections.singletonList(item);
}

// 类型推断(JDK 7+ 钻石操作符)
List<String> list = new ArrayList<>();          // 推断为 ArrayList<String>

// 类型推断(JDK 8+ 改进)
List<String> list = Collections.emptyList();     // 推断返回类型的泛型参数
process(Collections.emptyList());                // 根据方法参数推断

// 显式指定类型参数(通常不需要)
List<String> list = Collections.<String>emptyList();
1
2
3
4
5
6
7
8
9
10
11
12
13
14

JDK 8 的推断增强:

// JDK 7 中编译失败,JDK 8 中成功
public static <T> void process(List<T> list) { }

process(Collections.emptyList());
// JDK 7: 无法推断 T,需要写 process(Collections.<String>emptyList())
// JDK 8: 根据目标类型推断 T,编译通过
1
2
3
4
5
6

# 6.6.2 递归类型边界

// Comparable 的经典递归边界
public interface Comparable<T> {
    int compareTo(T o);
}

// 实现类声明自己可以与同类型比较
public class Student implements Comparable<Student> {
    @Override
    public int compareTo(Student other) { ... }
}

// 泛型方法中使用递归边界
public static <T extends Comparable<T>> T max(Collection<T> coll) {
    T max = null;
    for (T item : coll) {
        if (max == null || item.compareTo(max) > 0)
            max = item;
    }
    return max;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

更灵活的写法(处理继承场景):

// 问题:如果 Child extends Parent implements Comparable<Parent>
// max(List<Child>) 会编译失败,因为 Child 不满足 Comparable<Child>

// 解决:使用 ? super T
public static <T extends Comparable<? super T>> T max(Collection<? extends T> coll) {
    // Comparable<? super T>:T 可以与 T 的父类比较
    // Collection<? extends T>:可以传入 T 的子类集合
}
1
2
3
4
5
6
7
8

# 6.6.3 多重边界

// T 必须同时满足多个边界
public class DataProcessor<T extends Comparable<T> & Serializable & Cloneable> {
    // T 必须实现 Comparable、Serializable 和 Cloneable
}

// 注意:类边界必须放在第一位
public class Box<T extends Number & Comparable<T>> { }  // OK
public class Box<T extends Comparable<T> & Number> { }  // 编译错误!类在接口前面
1
2
3
4
5
6
7
8

# 6.6.4 泛型与可变参数

@SafeVarargs  // 抑制堆污染警告
public static <T> List<T> asList(T... items) {
    List<T> list = new ArrayList<>();
    for (T item : items) {
        list.add(item);
    }
    return list;
}

// 堆污染(Heap Pollution)问题
@SafeVarargs
static <T> T[] toArray(T... args) {
    return args;  // 看似正确
}

String[] arr = toArray("a", "b");
// 运行时可能抛出 ClassCastException
// 因为可变参数实际创建的是 Object[],而非 String[]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

@SafeVarargs 的含义:程序员承诺方法内部不会对泛型可变参数数组做不安全的操作(如写入不兼容类型或将其暴露给外部)。

# 6.7 运行时获取泛型信息

# 6.7.1 Signature属性保留泛型

虽然泛型在字节码指令层面被擦除,但 Class 文件的 Signature 属性中保留了泛型签名信息:

// 使用 javap -v 查看
Signature: #30  // Ljava/util/List<Ljava/lang/String;>;

// 这个信息存储在 Class 文件的属性表中,不影响字节码执行
// 但可以通过反射 API 读取
1
2
3
4
5

保留的位置:

  • 类声明中的泛型参数:class Dao<T> 的 T
  • 字段声明中的泛型:List<String> names 的 String
  • 方法签名中的泛型:List<T> getAll() 的 T

不保留的位置:

  • 方法内部的局部变量类型参数
  • 运行时创建的泛型实例

# 6.7.2 反射获取泛型参数

// 场景1:获取父类的泛型参数
public class UserDao extends BaseDao<User> { }

Type type = UserDao.class.getGenericSuperclass();
ParameterizedType pt = (ParameterizedType) type;
Type[] args = pt.getActualTypeArguments();
Class<?> entityClass = (Class<?>) args[0];
System.out.println(entityClass);  // class User

// 场景2:获取字段的泛型参数
public class Config {
    private List<String> names;
    private Map<String, Integer> scores;
}

Field namesField = Config.class.getDeclaredField("names");
ParameterizedType pt = (ParameterizedType) namesField.getGenericType();
System.out.println(pt.getActualTypeArguments()[0]); // class java.lang.String

Field scoresField = Config.class.getDeclaredField("scores");
ParameterizedType pt2 = (ParameterizedType) scoresField.getGenericType();
System.out.println(pt2.getActualTypeArguments()[0]); // class java.lang.String
System.out.println(pt2.getActualTypeArguments()[1]); // class java.lang.Integer

// 场景3:获取方法参数的泛型
public void process(List<String> names) { }

Method method = getClass().getMethod("process", List.class);
Type[] paramTypes = method.getGenericParameterTypes();
ParameterizedType pt = (ParameterizedType) paramTypes[0];
System.out.println(pt.getActualTypeArguments()[0]); // class java.lang.String
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

# 6.7.3 TypeToken模式

Gson 等库使用的经典模式,通过匿名子类捕获泛型信息:

// Gson 的 TypeToken
Type type = new TypeToken<List<User>>(){}.getType();
List<User> users = gson.fromJson(json, type);

// 原理:匿名子类在编译时会将泛型参数写入 Signature 属性
// new TypeToken<List<User>>(){} 创建了一个匿名类
// 这个匿名类继承 TypeToken<List<User>>
// 通过反射读取匿名类的父类泛型参数即可获取 List<User>
1
2
3
4
5
6
7
8

自己实现 TypeToken:

public abstract class TypeToken<T> {
    private final Type type;
    
    protected TypeToken() {
        Type superclass = getClass().getGenericSuperclass();
        this.type = ((ParameterizedType) superclass).getActualTypeArguments()[0];
    }
    
    public Type getType() { return type; }
}

// 使用
Type listOfString = new TypeToken<List<String>>(){}.getType();
System.out.println(listOfString);
// java.util.List<java.lang.String>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 6.7.4 框架中的泛型获取

框架 获取方式 用途
Spring ResolvableType 推断 Bean 的泛型类型
MyBatis TypeParameterResolver 推断 Mapper 方法的返回类型
Gson TypeToken JSON 反序列化目标类型
Jackson TypeReference JSON 反序列化目标类型
Guava TypeToken 通用的类型工具
// Spring 的 ResolvableType
ResolvableType type = ResolvableType.forClass(UserDao.class)
    .as(BaseDao.class);
Class<?> entityClass = type.getGeneric(0).resolve();
// entityClass = User.class

// Jackson 的 TypeReference
List<User> users = objectMapper.readValue(json, 
    new TypeReference<List<User>>(){});
1
2
3
4
5
6
7
8
9

# 6.8 泛型最佳实践与常见陷阱

# 6.8.1 泛型方法优于泛型类

// 不推荐:泛型类(限制了整个类的类型)
public class Converter<T> {
    public String convert(T input) { return input.toString(); }
}

// 推荐:泛型方法(更灵活,每次调用可以不同类型)
public class Converter {
    public <T> String convert(T input) { return input.toString(); }
}

Converter converter = new Converter();
converter.convert("hello");     // T = String
converter.convert(123);         // T = Integer
1
2
3
4
5
6
7
8
9
10
11
12
13

# 6.8.2 常见的泛型陷阱

陷阱一:泛型数组

// 错误示范
public <T> T[] toArray(List<T> list) {
    return (T[]) list.toArray();  // 实际返回 Object[]!
}

String[] arr = toArray(Arrays.asList("a", "b"));
// ClassCastException: Object[] cannot be cast to String[]

// 正确做法
public <T> T[] toArray(List<T> list, Class<T> clazz) {
    @SuppressWarnings("unchecked")
    T[] arr = (T[]) Array.newInstance(clazz, list.size());
    return list.toArray(arr);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

陷阱二:泛型方法重载

// 编译错误:擦除后签名相同
public void process(List<String> list) { }
public void process(List<Integer> list) { }
// 擦除后都是 process(List list),编译器报重复方法
1
2
3
4

陷阱三:原始类型与泛型混用

List rawList = new ArrayList<String>();  // 原始类型
rawList.add(123);                        // 编译通过,但类型安全被破坏

List<String> typedList = rawList;        // 编译警告
String s = typedList.get(0);             // 运行时 ClassCastException
1
2
3
4
5

# 6.8.3 类型安全的异构容器

// Effective Java 推荐的 TypeSafe Heterogeneous Container 模式
public class Favorites {
    private Map<Class<?>, Object> map = new HashMap<>();
    
    public <T> void put(Class<T> type, T instance) {
        map.put(type, type.cast(instance));  // 确保类型安全
    }
    
    public <T> T get(Class<T> type) {
        return type.cast(map.get(type));     // 安全强转
    }
}

Favorites f = new Favorites();
f.put(String.class, "hello");
f.put(Integer.class, 123);

String s = f.get(String.class);    // "hello",无需强转
Integer i = f.get(Integer.class);  // 123
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

这个模式被广泛应用于依赖注入框架中。

# 6.9 Java vs 其他语言的泛型对比

特性 Java(擦除式) C#(具化式) Kotlin Go 1.18+
运行时类型 不保留 保留 不保留(JVM限制) 部分保留
基本类型泛型 不支持 支持 不支持(内联类优化) 支持
new T() 不可以 可以 reified 内联函数支持 不直接支持
协变/逆变 使用处(通配符) 声明处 + 使用处 声明处(out/in) 不支持
向后兼容 完全兼容 不兼容旧 .NET 1.x N/A 兼容旧 Go
性能 有装箱开销 值类型无装箱 有装箱开销 有部分字典开销

Kotlin 的 reified 关键字:

// Kotlin 通过内联函数 + reified 突破擦除限制
inline fun <reified T> isType(value: Any): Boolean {
    return value is T  // 在 Java 中不可能!
}

// 原理:编译时将 T 替换为具体类型,内联到调用处
isType<String>("hello")
// 内联后变成:
"hello" is String
1
2
3
4
5
6
7
8
9

# 6.10 总结与核心要点

泛型设计哲学:

  1. 编译期安全,运行期擦除:用编译器的类型检查代替运行时的强制转换
  2. 向后兼容优先:擦除保证了泛型代码和旧字节码的兼容性,但牺牲了运行时类型信息
  3. PECS 原则:extends 用于读(生产者)、super 用于写(消费者),是泛型 API 设计的金科玉律
  4. Signature 属性:泛型信息虽然在指令层面擦除,但在类文件元数据中保留,框架可以通过反射读取

泛型限制速查表:

限制 原因 替代方案
不能 new T() 擦除后变成 new Object() 传入 Class<T> 或 Supplier<T>
不能 new T[] 数组需要运行时类型检查 Array.newInstance(clazz, size)
不能 instanceof List<String> 运行时无泛型信息 instanceof List<?>
不能 List<int> int 不是 Object 子类 List<Integer>(自动装箱)
不能声明泛型异常类 catch 需要运行时类型匹配 使用非泛型异常
静态字段不能用类泛型参数 泛型属于实例级别 静态泛型方法

核心要点:

问题 答案
什么是类型擦除 编译后泛型参数替换为上界(默认 Object)
桥接方法的作用 保证擦除后多态仍然正确
extends vs super extends 只读,super 只写
框架怎么获取泛型信息 Class 文件的 Signature 属性 + 反射 API
TypeToken 原理 匿名子类将泛型参数编码到 Signature 中

理解泛型擦除,就理解了 Java 类型系统最深层的设计取舍。

上次更新: 2026/06/10, 11:13:41
Object通用方法的契约
枚举原理与最佳实践

← Object通用方法的契约 枚举原理与最佳实践→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式