(一)Java 泛型擦除(Type Erasure)
Java 泛型擦除(Type Erasure)是 Java 泛型的一种底层实现机制。
它的核心含义是:Java 的泛型只在编译阶段生效,一旦代码编译成字节码(.class 文件),所有的泛型类型信息都会被"擦除"掉,JVM 在运行时根本不知道泛型的存在。
简单来说,泛型就像是 Java 编译器提供的一层"语法糖"或"伪装",它能帮你在写代码时做好类型检查,但到了程序真正跑起来的时候,这层伪装就被撕掉了。
01,🤔 为什么要设计泛型擦除?
核心原因是为了向下兼容 。
泛型是在 Java 5 版本才引入的。为了让使用了泛型的新代码,能够完美兼容 Java 5 之前没有泛型的老代码和老类库,同时保证旧的 JVM 不需要做任何修改就能运行新的泛型代码,Java 设计者选择了这种"编译期检查 + 运行期擦除"的折中方案。
02,⚙️ 泛型擦除是如何工作的?
编译器在编译代码时,会遵循以下两条核心规则进行"擦除":
- 无界泛型擦除为 Object :如果泛型没有指定边界,就会被替换成
Object。 - 有界泛型擦除为上界 :如果泛型指定了边界(例如
<T extends Number>),就会被替换成它的边界类型(即Number)。
03, 🤔 举个直观的例子:
假设你在代码中写了这样一个泛型集合:
java
List<String> list = new ArrayList<>();
list.add("hello");
String s = list.get(0);
经过编译器处理并擦除泛型后,实际生成的字节码逻辑等同于:
java
// 泛型 <String> 被擦除,变成了原始类型 List
List list = new ArrayList();
list.add("hello");
// 编译器自动插入了强制类型转换 (String)
String s = (String) list.get(0);
也就是说,编译器在帮你做完类型检查后,自动把泛型拿掉,并在取值的地方悄悄帮你加上了强制类型转换。
04,⚠️ 泛型擦除带来的常见限制
正是因为运行时泛型信息已经不存在了,所以 Java 中会出现一些看似奇怪的限制:
-
无法通过
instanceof判断泛型类型
因为运行时没有泛型信息,所以if (obj instanceof List<String>)这种写法是非法的,编译器会直接报错。你只能判断if (obj instanceof List)。 -
不能直接创建泛型数组
正如我们之前聊到的,new List<String>是不允许的。因为数组在运行时需要知道确切的元素类型来做安全检查,而泛型擦除后 JVM 无法得知具体的泛型类型。 -
不能直接实例化泛型对象
代码中不能写new T()。因为编译后T已经被擦除成了Object或某个上界,JVM 根本不知道T具体是哪个类,也就无法进行实例化。 -
无法重载仅泛型参数不同的方法
例如下面两个方法,编译后它们的参数都会变成List,方法签名完全一致,因此无法构成重载:java编辑
javavoid process(List<String> list) {} // 编译报错 void process(List<Integer> list) {}
总结一下: Java 泛型擦除是为了兼容老版本而做出的妥协。
它把类型安全检查的工作全部交给了编译器,从而保证了运行时的效率和兼容性,但也因此带来了一些使用上的限制。这也正是为什么在 Fastjson 等框架反序列化时,必须通过 TypeReference 这种特殊手段来"绕过"擦除,从而保留住泛型信息。
(二),Fastjson泛型擦除"踩坑点"
在使用 com.alibaba.fastjson.JSON(包括 Fastjson 1.x 和 Fastjson2)进行转换时,泛型擦除是一个非常经典且高频的"踩坑点"。
在实际业务开发中,泛型擦除引发以下几个典型的严重问题:
1. 嵌套泛型对象的反序列化类型丢失
在微服务或前后端交互中,我们通常会定义一个统一的响应包装类,比如 Result<T> 或 Response<T>。 当 T 本身又是一个泛型对象时,极易出错。
问题场景:
java
public class Result<T> {
private int code;
private T data;
}
// 期望反序列化为 Result<List<User>>
String json = "{\"code\":200,\"data\":[{\"id\":1,\"name\":\"张三\"}]}";
// ❌ 错误写法:泛型信息丢失
Result wrongResult = JSON.parseObject(json, Result.class);
// 此时 wrongResult.getData() 的实际类型会变成 JSONObject 或 LinkedHashMap,
// 后续如果直接强转或遍历,会抛出 ClassCastException。
解决方案:
使用 **TypeReference**来保留完整的嵌套泛型签名,保留泛型信息,安全、正确地获取强类型的 List 集合。
java
// ✅ 正确写法
Result<List<User>> correctResult = JSON.parseObject(json,
new TypeReference<Result<List<User>>>(){});
// 安全获取到具体的 User 对象列表
List<User> users = correctResult.getData();
机制原理:
巧妙利用匿名内部类保留类型信息, new TypeReference<List<User>>() {}这种写法(注意末尾的大括号 {})实际上是创建了一个继承自 TypeReference 的匿名内部类 。
虽然 Java 会擦除普通变量的泛型,但类的继承关系和泛型参数会被保留在编译后的字节码中 。Fastjson 在底层通过 Java 的反射机制,能够从这个匿名子类中提取出完整的泛型结构(即知道这是一个包含 **User**的 List),从而精准地把 JSON 数据还原成你需要的对象。
2. 运行时 instanceof 判断失效
由于泛型在编译后会被擦除,JVM 在运行时根本感知不到 `<String>` 或 `<Integer>` 的区别,因此不能直接对泛型进行 `instanceof` 检查。
问题代码:
java
List<String> list = new ArrayList<>();
// ❌ 编译直接报错:
// Cannot perform instanceof check against parameterized type List<String>
if (list instanceof List<String>) {
// ...
}
解决建议:
在运行时只能检查原始类型,例如使用 list instanceof List<?> 或 list instanceof List。
3. 无法创建泛型数组
同样是因为类型擦除,Java 不允许直接初始化带有泛型参数的数组。
问题代码:
java
// ❌ 编译报错:Cannot create a generic array of List<String>
List<String>[] stringListArr = new List<String>[] ;
解决建议:
通常建议使用 List<List<String>> 来代替泛型数组,或者先创建原始类型数组再进行强转(但不推荐,存在安全隐患)。
4. 动态泛型的高级处理(反射场景)
如果你是在写通用的工具类、RPC 框架解码器或 DAO 层,泛型的类型往往是在运行时通过参数动态传递的,这时无法直接写死 new TypeReference<...>。
解决方案:
可以使用 Fastjson 提供的 ParameterizedTypeImpl 手动构建泛型类型:
java
public <T> List<T> parseList(String json, Class<T> elementClass) {
// 手动构建 List<T> 的完整类型信息
Type listType = new ParameterizedTypeImpl(new Type[]{elementClass},
null, List.class);
return JSON.parseObject(json, listType);
}
这样在调用 parseList(jsonStr, User.class) 时,就能在动态场景下完美避开泛型擦除的坑。
总结一下:
只要涉及到 Fastjson 的反序列化(parseObject / parseArray),并且目标对象带有泛型(无论是 List<T>、Map<K,V> 还是自定义的 Result<T>),千万不要直接传 .class ,一定要通过 TypeReference 或 **ParameterizedTypeImpl**把完整的泛型结构"喂"给 Fastjson,这样才能从根本上杜绝类型转换异。
通过 TypeReference 这种特殊手段来"绕过"擦除,从而保留住泛型信息。