问题的起源

在解析 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的子类(比如IntegerDouble等),所以擦除时就用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是实例级别的
  • 不同实例可能有不同的TBox<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) { }  // 重复定义!

四、为什么会有类型擦除?

历史原因

  1. Java 1.5 才引入泛型(2004 年)
  2. 需要兼容旧版本代码
    • Java 1.0-1.4 的代码大量使用原始类型(ListMap
    • 新的泛型代码(List<String>)必须能和旧代码互操作
  3. 解决方案
    • 泛型只在编译期检查
    • 编译后擦除泛型信息
    • 字节码保持和旧版本兼容

示例

// 旧代码(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 来保留
匿名类继承,反射取泛型
历史兼容性,运行不知情

参考资料

更新: 2026-03-03 20:04:38
原文: https://www.yuque.com/dongpozhouzi-mshe3/zhm85g/ux65k7smu1kbxkzg