网易魔天记手游
374.22MB · 2025-12-10
在日常开发中,我们几乎离不开泛型: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];
虽然擦除了,但我们仍可以通过 反射 + 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。 |
避免在运行时依赖泛型类型信息
泛型的类型擦除意味着运行时无法区分泛型参数。
使用 Class 保存显式类型
public <T> T fromJson(String json, Class<T> clazz);
反射场景建议使用 TypeReference / TypeToken
如在 Gson、Jackson、MyBatis 中:
new TypeReference<List<User>>() {}
避免泛型数组、泛型静态变量
泛型静态变量是全类共享,不随类型参数变化。
| 特性 | 泛型阶段 | 擦除后类型 | 常见坑点 |
|---|---|---|---|
| 类泛型 | 编译期有效 | Object 或上界类型 | 无法反射到具体类型 |
| 方法泛型 | 编译期有效 | Object 参数 | 无法重载区分 |
| 泛型数组 | 不支持 | - | 编译错误 |
| 反射 | 可绕过类型检查 | - | 可能导致运行时 ClassCastException |