问题的起源
在解析 JSON 时,你会看到这样的代码:
// AdListServiceImpl.kt: 第 36-37 行
val type = object : TypeToken<FlowBean<AdListBean>>() {}.type
val flowBean: FlowBean<AdListBean>? = VideoGsonFactory.safeFromJson(result, type)疑问:为什么不直接写FlowBean::class.java?
问题:为什么不能直接写 class?
尝试 1:直接用 class(❌ 错误)
// ❌ 这样写会丢失泛型信息
val flowBean = gson.fromJson(result, FlowBean::class.java)
// ↑
// 这里只知道是 FlowBean,不知道泛型是什么
// 运行时,Gson 只知道:
// - 要解析成 FlowBean
// - 但不知道 FlowBean<T> 的 T 是什么
// - 所以无法解析 data 字段(不知道 data 是什么类型)问题示例:
data class FlowBean<T>(
val errno: Int,
val data: T // ❌ Gson 不知道 T 是什么类型!
)
// 使用 FlowBean::class.java 解析
val flowBean = gson.fromJson(result, FlowBean::class.java)
// flowBean.data 是什么类型?Gson 不知道!
// 最后 data 会被解析为 LinkedTreeMap(默认的 JSON Object)解析结果:
// 后端返回
{
"errno": 0,
"data": {
"items": [...]
}
}
// 使用 FlowBean::class.java 解析后
FlowBean(
errno = 0,
data = LinkedTreeMap(...) // ❌ data 被解析为 Map,而不是 AdListBean
)什么是类型擦除(Type Erasure)?
核心定义
Java 的泛型擦除(Type Erasure) 是 Java 语言在实现泛型时采用的一种编译期机制。
核心思想:
泛型信息只存在于编译期,运行时会被”擦除”,即 JVM 并不知道泛型的具体类型。
通俗理解:
Java 泛型只在编译期存在,运行时会被”擦除”,只剩下原始类型
一、什么是泛型擦除?
在 Java 中,泛型(如List<String>、Map<Integer, String>)仅用于编译时的类型检查和安全保证。一旦代码被编译成字节码(.class文件),所有的泛型类型参数都会被替换为其上界(通常是Object,除非显式指定)。
示例
// 编译前(源代码)
List<String> list1 = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();
// 编译后,JVM 看到的其实是
List list1 = new ArrayList();
List list2 = new ArrayList();也就是说,运行时无法区分List<String>和List<Integer>。它们都是List
验证代码:
val list1: List<String> = listOf("a", "b")
val list2: List<Int> = listOf(1, 2)
// 运行时
println(list1::class.java) // 输出: class java.util.Arrays$ArrayList
println(list2::class.java) // 输出: class java.util.Arrays$ArrayList
println(list1::class.java == list2::class.java) // true!两个都是 List此时是看不出List<String>和List<Int>的区别的。
二、泛型擦除的规则
规则 1:无界类型参数 → 擦除为 Object
// 源代码
public class Box<T> {
T value;
}
// 编译后(伪代码)
public class Box {
Object value; // T 被擦除为 Object
}为什么:因为T没说是什么类型,所以最保险的就是当成Object
规则 2:有界类型参数 → 擦除为第一个边界类型
// 源代码
public class Box<T extends Number> {
T value;
}
// 编译后(伪代码)
public class Box {
Number value; // T 被擦除为 Number(第一个边界)
}为什么:因为T只能是Number或者Number的子类(比如Integer、Double等),所以擦除时就用Number作为边界类型。
多个边界的情况:
// 源代码
public class Box<T extends Number & Comparable<T>> {
T value;
}
// 编译后
public class Box {
Number value; // 擦除为第一个边界 Number
}规则 3:方法签名中的泛型也会被擦除
// 源代码
public <T> T get(T obj) {
return obj;
}
// 编译后
public Object get(Object obj) { // T 被擦除为 Object
return obj;
}为什么:
- 泛型
<T>被去掉了 - 参数
T obj变成了Object obj - 因为所有引用类型都继承自
Object,所以用Object接收任何对象都可以
有界泛型的方法:
// 源代码
public <T extends Number> T max(T a, T b) {
return a.doubleValue() > b.doubleValue() ? a : b;
}
// 编译后
public Number max(Number a, Number b) { // T 被擦除为 Number
return a.doubleValue() > b.doubleValue() ? a : b;
}规则 4:桥接方法(Bridge Methods)
为了保证多态的正确性,编译器会自动生成”桥接方法”。
示例:
// 源代码
class MyList extends ArrayList<String> {
@Override
public boolean add(String s) {
System.out.println("Adding: " + s);
return super.add(s);
}
}
// 编译后,编译器自动生成桥接方法
class MyList extends ArrayList {
// 用户定义的方法
public boolean add(String s) {
System.out.println("Adding: " + s);
return super.add(s);
}
// 编译器自动生成的桥接方法
public boolean add(Object o) {
return add((String) o); // 强转 + 调用实际方法
}
}为什么需要桥接方法:
ArrayList<String> list = new MyList();
list.add("test");
// ArrayList 的 add 方法签名(擦除后):boolean add(Object o)
// MyList 的 add 方法签名:boolean add(String s)
// 签名不同!多态会失效
// 所以编译器生成桥接方法:boolean add(Object o)
// 保证多态正常工作验证桥接方法:
MyList list = new MyList();
// 查看方法
list.javaClass.declaredMethods.forEach { method ->
println("${method.name}: ${method.parameterTypes.contentToString()}")
}
// 输出:
// add: [class java.lang.String] ← 用户定义的
// add: [class java.lang.Object] ← 编译器生成的桥接方法注意:
每个对象知道自己是什么对象,所以可以由编译器来做类型强转。比如
System.out.println(obj),就会调用自己的toString()方法。
三、泛型擦除带来的限制
限制 1:不能使用 instanceof 判断具体泛型类型
List<String> list = new ArrayList<>();
// ❌ 编译错误
if (list instanceof List<String>) { // 错误!
// ...
}
// ✅ 只能判断原始类型
if (list instanceof List) { // 正确
// 但不知道是 List<String> 还是 List<Integer>
}原因:运行时泛型信息已被擦除,JVM 不知道是List<String>还是List<Integer>
限制 2:不能创建泛型数组
// ❌ 编译错误
T[] arr = new T[10];
// ❌ 编译错误
List<String>[] lists = new ArrayList<String>[10];原因:
// 如果允许创建泛型数组,会有类型安全问题
Object[] arr = new String[10];
arr[0] = 1; // 运行时错误!ArrayStoreException
// 泛型数组会让这个问题更隐蔽
List<String>[] lists = new ArrayList<String>[10];
Object[] arr = lists;
arr[0] = new ArrayList<Integer>(); // 编译通过,但类型错误!解决方案:
// 方案 1:使用 ArrayList 代替数组
List<T> list = new ArrayList<>();
// 方案 2:创建原始类型数组,然后强转
@SuppressWarnings("unchecked")
T[] arr = (T[]) new Object[10];
// 方案 3:传入 Class<T> 对象,使用反射
@SuppressWarnings("unchecked")
T[] arr = (T[]) Array.newInstance(clazz, 10);限制 3:不能直接实例化泛型类型
// ❌ 编译错误
T obj = new T();
// ❌ 编译错误
public <T> T create() {
return new T();
}原因:运行时T已被擦除为Object,JVM 不知道要创建什么类型的对象
解决方案:
方案 1:传入 Class** 对象(反射)**
public <T> T create(Class<T> clazz) throws Exception {
return clazz.newInstance();
}
// 使用
String str = create(String.class);方案 2:使用工厂模式
interface Factory<T> {
T create();
}
public <T> T create(Factory<T> factory) {
return factory.create();
}
// 使用
String str = create(() -> new String());方案 3:Kotlin 的 reified(推荐)
inline fun <reified T> create(): T {
return T::class.java.newInstance()
}
// 使用
val str = create<String>()限制 4:运行时无法获取泛型的实际类型
public <T> void printType(T obj) {
// ❌ 无法直接获取 T 的类型
System.out.println(T.class); // 编译错误
// ✅ 只能获取对象的实际类型
System.out.println(obj.getClass());
}
printType("hello"); // 输出: class java.lang.String
printType(123); // 输出: class java.lang.Integer但可以通过反射 + TypeToken 等技巧间接获取(如在字段/方法签名中保留的泛型信息):
class MyClass<T> {
T value;
}
// 通过反射获取字段的泛型信息
Field field = MyClass.class.getDeclaredField("value");
Type type = field.getGenericType();
System.out.println(type); // 输出: T(保留在字段签名中)限制 5:不能在静态上下文中使用类型参数
class Box<T> {
// ❌ 编译错误
private static T value;
// ❌ 编译错误
public static T getValue() {
return value;
}
}原因:
- 静态成员属于类,不属于实例
- 类型参数
T是实例级别的 - 不同实例可能有不同的
T:Box<String>、Box<Integer>
限制 6:重载方法签名冲突
class Test {
// 看起来是两个不同的方法
public void method(List<String> list) { }
public void method(List<Integer> list) { } // ❌ 编译错误!
}
// 因为擦除后,两个方法的签名完全一样
public void method(List list) { }
public void method(List list) { } // 重复定义!四、为什么会有类型擦除?
历史原因
- Java 1.5 才引入泛型(2004 年)
- 需要兼容旧版本代码:
- Java 1.0-1.4 的代码大量使用原始类型(
List、Map) - 新的泛型代码(
List<String>)必须能和旧代码互操作
- Java 1.0-1.4 的代码大量使用原始类型(
- 解决方案:
- 泛型只在编译期检查
- 编译后擦除泛型信息
- 字节码保持和旧版本兼容
示例:
// 旧代码(Java 1.4)
List list = new ArrayList();
list.add("test");
list.add(123);
// 新代码(Java 1.5+)
List<String> list = new ArrayList<>();
list.add("test");
// list.add(123); // 编译错误
// 编译后,两者的字节码一样
// 这样新旧代码可以互相调用TypeToken 的解决方案
什么是 TypeToken?
TypeToken 是 Gson 提供的一个工具类,用于保留泛型信息。
val type = object : TypeToken<FlowBean<AdListBean>>() {}.type
val flowBean = gson.fromJson(result, type)这行代码做了什么?
步骤 1:创建匿名内部类
object : TypeToken<FlowBean<AdListBean>>() {}
// ↑
// 继承 TypeToken,并指定泛型步骤 2:泛型信息被保留在匿名类中
// 匿名内部类会保留父类的泛型信息
// 因为它是一个实际的类定义
// 等价于:
class MyTypeToken : TypeToken<FlowBean<AdListBean>>()
val type = MyTypeToken().type步骤 3:通过反射获取泛型信息
// TypeToken 内部使用反射
// 从匿名类的父类泛型参数中提取 FlowBean<AdListBean>为什么 TypeToken 能保留泛型信息?
关键原理:类的继承关系保留泛型信息
虽然运行时泛型会被擦除,但类的继承关系是保留的!
// ❌ 直接使用变量,泛型被擦除
List<String> list = new ArrayList<>();
list.getClass() // 只知道是 ArrayList,不知道 <String>
// ✅ 通过子类,可以获取泛型信息
class StringList extends ArrayList<String> { }
StringList list = new StringList();
// 通过反射获取父类的泛型信息
Type superclass = StringList.class.getGenericSuperclass();
// 输出: ArrayList<String> ← 泛型信息保留了!TypeToken 内部实现(简化)
abstract class TypeToken<T> {
private final Type type;
protected TypeToken() {
// 1. 获取当前类的父类(带泛型信息)
Type superclass = getClass().getGenericSuperclass();
// superclass = TypeToken<FlowBean<AdListBean>>
// 2. 提取泛型参数
ParameterizedType parameterized = (ParameterizedType) superclass;
this.type = parameterized.getActualTypeArguments()[0];
// type = FlowBean<AdListBean>
}
public Type getType() {
return type;
}
}使用时:
// 创建匿名内部类
val typeToken = object : TypeToken<FlowBean<AdListBean>>() {}
// TypeToken 内部通过反射获取泛型信息
val type = typeToken.type // 得到 FlowBean<AdListBean>图解原理
代码:
val type = object : TypeToken<FlowBean<AdListBean>>() {}.type
┌─────────────────────────────────────────────────────────┐
│ 步骤 1: 创建匿名内部类 │
│ │
│ ┌───────────────────────────────────────┐ │
│ │ 匿名类 extends TypeToken<...> │ │
│ │ │ │
│ │ 泛型信息: FlowBean<AdListBean> │ ← 保留在类定义中│
│ └───────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 步骤 2: TypeToken 通过反射读取 │
│ │
│ ┌───────────────────────────────────────┐ │
│ │ TypeToken 内部逻辑 │ │
│ │ │ │
│ │ val superclass = getGenericSuperclass() │
│ │ // 得到: TypeToken<FlowBean<AdListBean>> │
│ │ │ │
│ │ val type = actualTypeArguments[0] │ │
│ │ // 提取: FlowBean<AdListBean> │ │
│ └───────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 步骤 3: 返回完整的类型信息 │
│ │
│ Type = FlowBean<AdListBean> ← 泛型信息完整保留 │
└─────────────────────────────────────────────────────────┘实际例子对比
例子 1:解析 List
// JSON
val json = """["a", "b", "c"]"""
// ❌ 错误方式
val list1 = gson.fromJson(json, List::class.java)
// list1 的类型是 List<Any?>
// 元素类型丢失
println(list1[0]::class.java) // 输出: String(运行时才知道)
val item: String = list1[0] // ❌ 编译错误:类型不匹配
// ✅ 正确方式
val type = object : TypeToken<List<String>>() {}.type
val list2 = gson.fromJson(json, type)
// list2 的类型是 List<String>
// 元素类型保留
val item: String = list2[0] // ✅ 编译通过例子 2:解析嵌套泛型
// JSON
val json = """
{
"errno": 0,
"data": {
"items": [
{"title": "广告1", "title_cmd": "xxx"}
]
}
}
"""
// ❌ 错误方式
val result1 = gson.fromJson(json, FlowBean::class.java)
println(result1.data::class.java) // 输出: LinkedTreeMap
// result1.data 是 LinkedTreeMap
// 泛型丢失,无法正确解析
// ✅ 正确方式
val type = object : TypeToken<FlowBean<AdListBean>>() {}.type
val result2 = gson.fromJson(json, type)
println(result2.data::class.java) // 输出: AdListBean
// result2.data 是 AdListBean
// 泛型保留,正确解析例子 3:解析 Map
// JSON
val json = """
{
"user1": {"name": "张三", "age": 20},
"user2": {"name": "李四", "age": 25}
}
"""
data class User(val name: String, val age: Int)
// ❌ 错误方式
val map1 = gson.fromJson(json, Map::class.java)
// map1 的类型是 Map<Any?, Any?>
// key 和 value 类型都丢失
println(map1["user1"]::class.java) // 输出: LinkedTreeMap
// ✅ 正确方式
val type = object : TypeToken<Map<String, User>>() {}.type
val map2 = gson.fromJson(json, type)
// map2 的类型是 Map<String, User>
// key 是 String,value 是 User
println(map2["user1"]::class.java) // 输出: User为什么要写成这样?
完整语法拆解
val type = object : TypeToken<FlowBean<AdListBean>>() {}.type
// ↑ ↑ ↑ ↑ ↑ ↑
// │ │ │ │ │ └─ 获取 Type 对象
// │ │ │ │ └───── 空类体(不需要实现任何方法)
// │ │ └───────────────────────────┴──────── 指定泛型参数
// │ └──────────────────────────────────── 继承 TypeToken
// └─────────────────────────────────────────── 创建匿名对象等价写法
写法 1:匿名对象(常用)
val type = object : TypeToken<FlowBean<AdListBean>>() {}.type写法 2:命名类(理解用)
class MyToken : TypeToken<FlowBean<AdListBean>>()
val type = MyToken().type写法 3:完全展开(理解用)
val typeToken = object : TypeToken<FlowBean<AdListBean>>() {}
val type = typeToken.type写法 4:使用 Kotlin 扩展函数(简化)
// 定义扩展函数
inline fun <reified T> typeOf(): Type {
return object : TypeToken<T>() {}.type
}
// 使用
val type = typeOf<FlowBean<AdListBean>>()其他序列化库的解决方案
Moshi (类似 Gson)
val moshi = Moshi.Builder().build()
// 使用 Kotlin 扩展函数(内部用 reified 泛型)
val adapter = moshi.adapter<FlowBean<AdListBean>>()
val flowBean = adapter.fromJson(json)
// 不需要 TypeToken,因为 Kotlin 的 reified 可以保留泛型信息Moshi 的实现:
// Moshi 内部实现(简化)
inline fun <reified T> Moshi.adapter(): JsonAdapter<T> {
return adapter(T::class.java)
}
// reified 让泛型信息在运行时可用kotlinx.serialization (Kotlin 官方)
import kotlinx.serialization.*
import kotlinx.serialization.json.*
@Serializable
data class FlowBean<T>(val errno: Int, val data: T)
@Serializable
data class AdListBean(val items: List<AdListItemDataBean>)
// 使用 reified 泛型,不需要 TypeToken
val flowBean = Json.decodeFromString<FlowBean<AdListBean>>(json)优势:
- Kotlin 原生支持
- 使用
reified泛型,不需要 TypeToken - 编译期生成序列化代码,性能更好
Jackson (Java 常用)
val mapper = ObjectMapper()
// 使用 TypeReference(类似 TypeToken)
val type = object : TypeReference<FlowBean<AdListBean>>() {}
val flowBean = mapper.readValue(json, type)TypeToken vs reified 对比
TypeToken(Gson 的方案)
// 需要创建匿名对象
val type = object : TypeToken<FlowBean<AdListBean>>() {}.type
val flowBean = gson.fromJson(json, type)特点:
- ✅ 兼容 Java(因为 Java 没有 reified)
- ❌ 语法繁琐(需要
object :和{}) - ✅ 运行时通过反射获取泛型信息
reified(Kotlin 的方案)
// 使用 reified 泛型
inline fun <reified T> Gson.fromJson(json: String): T {
return fromJson(json, T::class.java)
}
// 使用
val flowBean = gson.fromJson<FlowBean<AdListBean>>(json)特点:
- ❌ 只能在 Kotlin 中使用(Java 不支持)
- ✅ 语法简洁(不需要 TypeToken)
- ✅ 编译期内联,性能更好
reified 的原理:
// 源代码
inline fun <reified T> test() {
println(T::class.java)
}
test<String>()
// 编译后(内联展开)
println(String::class.java) // 泛型信息被内联进代码限制:
reified只能用于inline函数inline函数会被内联到调用处- 所以泛型信息可以在编译时”嵌入”到代码中
项目中的实际应用
代码位置
位置: flowvideo/ad/api/AdListServiceImpl.kt: 第 36-37 行
override fun parseResponse(response: Response?, state: Int, statRecord: NetworkStatRecord?): Result<AdListBean>? {
if (state == Status.HTTP_OK) {
val result = response?.body()?.string()
if (result?.isNotBlank() == true) {
// 定义解析类型(使用 TypeToken)
val type = object : TypeToken<FlowBean<AdListBean>>() {}.type
// 使用 Gson 解析
val flowBean: FlowBean<AdListBean>? = VideoGsonFactory.safeFromJson(result, type)
return flowBean?.getRealData()?.let { data ->
Result.Success(data)
}
}
}
return Result.Failure(Throwable("AdServiceImpl: response body is null"))
}为什么这里必须用 TypeToken?
// 后端返回的 JSON 结构
{
"errno": 0,
"errmsg": "",
"data": { // ← 这是 AdListBean
"items": [...],
"ad_policy": {...}
}
}
// 对应的数据结构
data class FlowBean<T>(
val errno: Int,
val errmsg: String,
val data: T // ← T 是泛型,必须用 TypeToken 指定
)
// 如果直接用 FlowBean::class.java
val flowBean = gson.fromJson(json, FlowBean::class.java)
// ❌ data 会被解析为 LinkedTreeMap,而不是 AdListBean
// 原因:泛型擦除,Gson 不知道 T 是什么
// 使用 TypeToken
val type = object : TypeToken<FlowBean<AdListBean>>() {}.type
val flowBean = gson.fromJson(json, type)
// ✅ data 正确解析为 AdListBean
// 原因:TypeToken 保留了泛型信息常见问题
Q1: 为什么不直接在 FlowBean 里写死类型?
// 方案 1:写死类型(不灵活)
data class AdFlowBean(
val errno: Int,
val data: AdListBean // 写死了
)
// 方案 2:使用泛型(灵活)
data class FlowBean<T>(
val errno: Int,
val data: T // 可以复用
)
// 可以用于不同场景
FlowBean<AdListBean> // 广告列表
FlowBean<AdDetailBean> // 广告详情
FlowBean<UserBean> // 用户信息答案:泛型让代码更灵活、可复用
Q2: 能不能简化 TypeToken 的写法?
可以!定义扩展函数:
// 定义扩展函数
inline fun <reified T> Gson.fromJsonTyped(json: String): T? {
val type = object : TypeToken<T>() {}.type
return fromJson(json, type)
}
// 使用(简化了)
val flowBean = gson.fromJsonTyped<FlowBean<AdListBean>>(json)Q3: TypeToken 有性能问题吗?
有轻微的性能开销:
- 每次创建匿名对象
- 反射获取泛型信息
优化方案:
// 缓存 Type 对象
companion object {
private val TYPE_FLOW_BEAN_AD_LIST = object : TypeToken<FlowBean<AdListBean>>() {}.type
}
// 使用
val flowBean = gson.fromJson(json, TYPE_FLOW_BEAN_AD_LIST)实际影响:
- 对于大多数应用,性能影响可以忽略
- 如果频繁解析,可以考虑缓存
Q4: 为什么 Kotlin 的 List 不会被擦除?
误区:Kotlin 的泛型也会被擦除!
val list1: List<String> = listOf("a")
val list2: List<Int> = listOf(1)
list1::class.java == list2::class.java // true!都是 ArrayList但 Kotlin 有 reified 可以解决部分问题:
inline fun <reified T> printType() {
println(T::class.java)
}
printType<String>() // 输出: class java.lang.String
printType<Int>() // 输出: int记忆口诀
泛型被擦除,TypeToken 来保留
匿名类继承,反射取泛型
历史兼容性,运行不知情
参考资料
- Gson User Guide - Generic Types
- Java Type Erasure
- Kotlin Reified Type Parameters
- Effective Java - Item 33: Consider typesafe heterogeneous containers
更新: 2026-03-03 20:04:38
原文: https://www.yuque.com/dongpozhouzi-mshe3/zhm85g/ux65k7smu1kbxkzg