在日常开发中,我们几乎离不开泛型:List<String>Map<Integer, User>Optional<T>……
但你知道吗?这些看似“类型安全”的泛型,在运行时其实都被“擦掉”了

今天,我们从底层出发,一文搞懂:

  • 泛型擦除机制到底是什么
  • ️ 编译器如何“假装”类型安全
  • 反射下的泛型陷阱与越界问题
  • 如何安全使用泛型 + 常见避坑方案

一、泛型为什么会被“擦除”?

Java 的泛型是 伪泛型(Type Erasure),这是为了 向下兼容 JDK1.4 之前的字节码

在编译阶段,所有泛型信息(T, E, K, V 等)都会被擦除,
最终生成的字节码中,泛型参数会被替换为其 上界(Upper Bound) 类型。

来看一个例子:

List<String> list = new ArrayList<>();
list.add("hello");

// 编译后的字节码近似等价于:
List list = new ArrayList();
list.add("hello");

运行时,list 已经不再知道它是 List<String>,而只是一个普通的 List


二、泛型擦除的三种形式

泛型声明擦除后类型说明
class Box<T>class Box无上界时默认擦为 Object
class Box<T extends Number>class Box(T→Number)擦为上界类型
class Box<T extends Comparable<T> & Serializable>擦为第一个上界 Comparable多上界时只保留第一个接口

示例:

public class Box<T extends Number> {
    T value;
    public void set(T value) { this.value = value; }
}

三、泛型方法的擦除

public <T> void print(T item) {
    System.out.println(item);
}

所以泛型方法并不会生成多个方法版本(不像 C++ 模板那样)。


四、反射下的“泛型陷阱”

泛型在编译期有效,但反射绕过了编译器的检查。
因此,泛型容器在运行时其实是“不设防”的。

来看一个著名的反例:

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

public class GenericTrap {
    public static void main(String[] args) throws Exception {
        List<String> list = new ArrayList<>();
        list.add("Java");

        Method add = list.getClass().getMethod("add", Object.class);
        add.invoke(list, 123); //  居然能加 Integer!

        System.out.println(list);
    }
}

输出:

[Java, 123]

五、泛型数组也是坑点之一

泛型擦除导致 无法直接创建泛型数组

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

为什么?

数组在运行时需要知道元素的精确类型,而泛型类型在编译后已被擦除。

解决方案:

@SuppressWarnings("unchecked")
List<String>[] arr = (List<String>[]) new ArrayList<?>[10];

六、通过反射获取泛型类型(TypeToken 技巧)

虽然擦除了,但我们仍可以通过 反射 + Type API 获取部分泛型信息:

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;

public class GenericTypeDemo<T> {
    public static void main(String[] args) {
        new GenericTypeDemo<String>().printType();
    }

    public void printType() {
        Type superClass = getClass().getGenericSuperclass();
        System.out.println(superClass);
    }
}

输出:

GenericTypeDemo<java.lang.String>

原理:JVM 在 类的继承结构 中仍保留泛型签名,可通过反射获取。


七、常见面试陷阱 ️

面试问题正确答案
List<Integer>List<String> 是否相同? 运行时相同,编译期不同。
为什么不能创建泛型数组?因为类型擦除 + 数组协变冲突。
泛型是编译期机制还是运行时机制?纯编译期机制(Type Erasure)。
如何在运行时拿到泛型类型?使用反射 ParameterizedType 或 TypeToken。

八、最佳实践与避坑建议

  1. 避免在运行时依赖泛型类型信息
    泛型的类型擦除意味着运行时无法区分泛型参数。

  2. 使用 Class 保存显式类型

    public <T> T fromJson(String json, Class<T> clazz);
    
  3. 反射场景建议使用 TypeReference / TypeToken
    如在 Gson、Jackson、MyBatis 中:

    new TypeReference<List<User>>() {}
    
  4. 避免泛型数组、泛型静态变量
    泛型静态变量是全类共享,不随类型参数变化。


九、总结

特性泛型阶段擦除后类型常见坑点
类泛型编译期有效Object 或上界类型无法反射到具体类型
方法泛型编译期有效Object 参数无法重载区分
泛型数组不支持-编译错误
反射可绕过类型检查-可能导致运行时 ClassCastException


本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:[email protected]