Java的泛型是在编译期生效的类型机制,运行时会触发类型擦除,将泛型参数的具体类型替换为上界类型,这就导致Jackson、Gson等JSON序列化框架在运行时无法直接获取泛型字段的具体类型信息,进而引发序列化映射错误。比如将List<User>序列化后再反序列化,框架可能无法识别内部元素是User类型,反而转为通用的Map结构。

类型擦除带来的泛型序列化问题
我们先通过一个简单的示例看类型擦除引发的典型问题,定义一个包含泛型字段的实体类:
public class Result<T> {
private int code;
private String msg;
private T data;
// 省略构造方法、getter、setter
}
如果直接对Result<User>对象进行序列化再反序列化,不使用任何类型保留手段,框架会丢失data字段的泛型类型信息。我们分别用Jackson和Gson测试这个问题:
Jackson默认反序列化问题
import com.fasterxml.jackson.databind.ObjectMapper;
public class JacksonTest {
public static void main(String[] args) throws Exception {
ObjectMapper objectMapper = new ObjectMapper();
// 构造Result<User>对象
User user = new User("张三", 20);
Result<User> result = new Result<>(200, "成功", user);
// 序列化为JSON字符串
String json = objectMapper.writeValueAsString(result);
System.out.println("序列化结果:" + json);
// 直接反序列化,不指定泛型类型
Result<User> deserialized = objectMapper.readValue(json, Result.class);
// 此时data字段会被转为LinkedHashMap,而不是User类型
System.out.println(deserialized.getData().getClass()); // 输出class java.util.LinkedHashMap
}
}
class User {
private String name;
private int age;
// 省略构造方法、getter、setter
}
class Result<T> {
private int code;
private String msg;
private T data;
public Result(int code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
// 省略getter、setter
}
Gson默认反序列化问题
import com.google.gson.Gson;
public class GsonTest {
public static void main(String[] args) {
Gson gson = new Gson();
User user = new User("张三", 20);
Result<User> result = new Result<>(200, "成功", user);
String json = gson.toJson(result);
System.out.println("序列化结果:" + json);
// 直接反序列化,不指定泛型类型
Result<User> deserialized = gson.fromJson(json, Result.class);
// 同样data字段会被转为LinkedTreeMap
System.out.println(deserialized.getData().getClass()); // 输出class com.google.gson.internal.LinkedTreeMap
}
}
Jackson处理泛型擦除的解决方案
Jackson提供了TypeReference类来保留泛型类型信息,它可以在编译期捕获泛型的具体类型,让运行时也能获取到完整的泛型参数。
使用TypeReference指定泛型类型
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
public class JacksonFixTest {
public static void main(String[] args) throws Exception {
ObjectMapper objectMapper = new ObjectMapper();
User user = new User("张三", 20);
Result<User> result = new Result<>(200, "成功", user);
String json = objectMapper.writeValueAsString(result);
// 使用TypeReference指定完整的泛型类型
Result<User> deserialized = objectMapper.readValue(json, new TypeReference<Result<User>>() {});
// 此时data字段正确映射为User类型
System.out.println(deserialized.getData().getClass()); // 输出class User
User data = deserialized.getData();
System.out.println(data.getName()); // 输出张三
}
}
处理嵌套泛型场景
如果是嵌套泛型比如Result<List<User>>,同样可以用TypeReference处理:
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Arrays;
public class JacksonNestedTest {
public static void main(String[] args) throws Exception {
ObjectMapper objectMapper = new ObjectMapper();
Result<List<User>> result = new Result<>(200, "成功", Arrays.asList(
new User("张三", 20),
new User("李四", 22)
));
String json = objectMapper.writeValueAsString(result);
// 嵌套泛型的TypeReference
Result<List<User>> deserialized = objectMapper.readValue(json, new TypeReference<Result<List<User>>>() {});
System.out.println(deserialized.getData().get(0).getName()); // 输出张三
}
}
Gson处理泛型擦除的解决方案
Gson对应的是TypeToken类,作用和Jackson的TypeReference类似,通过匿名内部类的方式在编译期捕获泛型类型信息。
使用TypeToken指定泛型类型
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
public class GsonFixTest {
public static void main(String[] args) {
Gson gson = new Gson();
User user = new User("张三", 20);
Result<User> result = new Result<>(200, "成功", user);
String json = gson.toJson(result);
// 通过TypeToken获取泛型类型
Type type = new TypeToken<Result<User>>() {}.getType();
Result<User> deserialized = gson.fromJson(json, type);
// data字段正确映射为User类型
System.out.println(deserialized.getData().getClass()); // 输出class User
System.out.println(deserialized.getData().getName()); // 输出张三
}
}
处理Map等泛型容器场景
对于Map<String, User>这类泛型容器,同样可以用TypeToken处理:
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;
public class GsonMapTest {
public static void main(String[] args) {
Gson gson = new Gson();
Map<String, User> map = new HashMap<>();
map.put("user1", new User("张三", 20));
String json = gson.toJson(map);
// 指定Map的泛型类型
Type type = new TypeToken<Map<String, User>>() {}.getType();
Map<String, User> deserialized = gson.fromJson(json, type);
System.out.println(deserialized.get("user1").getName()); // 输出张三
}
}
两种框架的方案对比
我们可以通过表格对比两种框架处理泛型擦除的方案特点:
| 对比项 | Jackson | Gson |
|---|---|---|
| 核心工具类 | TypeReference | TypeToken |
| 使用方式 | 作为readValue方法的第二个参数传入 | 先获取Type对象,再作为fromJson方法的第二个参数传入 |
| 适用场景 | 所有需要保留泛型类型的反序列化场景 | 所有需要保留泛型类型的反序列化场景 |
| 泛型嵌套支持 | 支持,直接在TypeReference中写嵌套泛型即可 | 支持,直接在TypeToken中写嵌套泛型即可 |
注意事项
- TypeReference和TypeToken都需要通过匿名内部类的方式创建,不能直接new一个实例,否则无法捕获泛型类型信息。
- 如果是在接口响应序列化场景,比如Spring MVC中使用Jackson,可以通过自定义返回类型或者配置全局的泛型类型处理器,避免每次手动指定TypeReference。
- 尽量避免在泛型字段中嵌套多层复杂的泛型结构,会增加类型信息保留的成本,也容易引发序列化错误。
只要理解了类型擦除的原理,并且学会使用框架提供的类型保留工具,泛型字段的JSON序列化映射问题就可以轻松解决,不会再出现反序列化后类型丢失的问题。