在日常开发中,我们几乎离不开泛型:List、Map、Optional……
但你知道吗?这些看似“类型安全”的泛型,在运行时其实都被“擦掉”了。
今天,我们从底层出发,一文搞懂:
一、泛型为什么会被“擦除”?
Java 的泛型是 伪泛型(Type Erasure),这是为了 向下兼容 JDK1.4 之前的字节码。
在编译阶段,所有泛型信息(T, E, K, V 等)都会被擦除,
最终生成的字节码中,泛型参数会被替换为其 上界(Upper Bound) 类型。
来看一个例子:
List list = new ArrayList();
list.add(\"hello\");
// 编译后的字节码近似等价于:
List list = new ArrayList();
list.add(\"hello\");
运行时,list 已经不再知道它是 List,而只是一个普通的 List。
二、泛型擦除的三种形式
| 泛型声明 | 擦除后类型 | 说明 |
|---|---|---|
class Box |
class Box |
无上界时默认擦为 Object |
class Box |
class Box(T→Number) |
擦为上界类型 |
class Box<T extends Comparable & Serializable> |
擦为第一个上界 Comparable |
多上界时只保留第一个接口 |
示例:
public class Box<T extends Number> {
T value;
public void set(T value) { this.value = value; }
}
三、泛型方法的擦除
public 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 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 {
public static void main(String[] args) {
new GenericTypeDemo().printType();
}
public void printType() {
Type superClass = getClass().getGenericSuperclass();
System.out.println(superClass);
}
}
输出:
GenericTypeDemo<java.lang.String>
原理:JVM 在 类的继承结构 中仍保留泛型签名,可通过反射获取。
七、常见面试陷阱 ️
| 面试问题 | 正确答案 |
|---|---|
List 和 List 是否相同? |
运行时相同,编译期不同。 |
| 为什么不能创建泛型数组? | 因为类型擦除 + 数组协变冲突。 |
| 泛型是编译期机制还是运行时机制? | 纯编译期机制(Type Erasure)。 |
| 如何在运行时拿到泛型类型? | 使用反射 ParameterizedType 或 TypeToken。 |
八、最佳实践与避坑建议
-
避免在运行时依赖泛型类型信息
泛型的类型擦除意味着运行时无法区分泛型参数。 -
使用 Class 保存显式类型
public T fromJson(String json, Class clazz); -
反射场景建议使用 TypeReference / TypeToken
如在 Gson、Jackson、MyBatis 中:new TypeReference<List<User>>() {} -
避免泛型数组、泛型静态变量
泛型静态变量是全类共享,不随类型参数变化。
九、总结
| 特性 | 泛型阶段 | 擦除后类型 | 常见坑点 |
|---|---|---|---|
| 类泛型 | 编译期有效 | Object 或上界类型 | 无法反射到具体类型 |
| 方法泛型 | 编译期有效 | Object 参数 | 无法重载区分 |
| 泛型数组 | 不支持 | – | 编译错误 |
| 反射 | 可绕过类型检查 | – | 可能导致运行时 ClassCastException |



还没有评论呢,快来抢沙发~