1. 什么是“擦除”?为什么会有
-
定义:JVM 上的泛型主要是编译期特性。编译器会把 List、Box 等在字节码层擦成原始类型(如 List、Box),把类型参数替换为其上界(T extends Number→Number)。
-
原因:要与老版本字节码/类库(没有泛型)保持二进制兼容;JVM 指令集并不区分 List 与 List。
-
直接后果
-
运行时看不到大多数实参类型(String 那一层没了)。
-
代码层面一堆限制:不能 new T()、不能 new T[]、不能 instanceof List、方法重载不能仅靠泛型参数不同等。
-
擦除不是“什么都没了”。声明处(类/字段/方法签名)仍保留在 Signature 属性里,反射可读取;但对象实例本身不携带“我是 List”的运行时信息。
2. 运行时到底还剩什么信息
-
类信息 Class<?> :总能拿到原始类(ArrayList.class)。
-
声明处泛型签名(反射可见):
-
Field#getGenericType()、Method#getGenericReturnType() 等可得到 Type:
- Class / ParameterizedType(如 List)
- TypeVariable(如 T)
- WildcardType(如 ? extends Number)
- GenericArrayType
-
-
数组是“可反射”的:new String[3] 运行时知道组件类型是 String(这也是为什么 Java 数组协变并可能抛 ArrayStoreException)。
3. 哪些类型是“可实化(reifiable)”的(Java 术语)
可实化= 运行时完整保留类型信息,能安全做 instanceof、数组创建等。
- ✅ 非泛型类型:String、int、Runnable
- ✅ 无界通配:List<?>(运行时只需知道“这是个 List”)
- ✅ 原始类型(raw type):List(不安全,尽量别用)
- ✅ 数组的组件如果可实化,则数组也可实化:String[]
- ❌ 参数化类型:List、Map<K,V> —— 不可实化****
- ❌ 类型变量:T —— 不可实化
4. 既然被擦除了,
什么时候还能拿到“真实类型”?
4.1 显式携带“类型令牌”(最可靠)
- Java:传 Class 或更强的 Type(ParameterizedType)
<T> T fromJson(String json, Type type); // Gson / Jackson 常用
fromJson(json, new TypeToken<List<User>>(){}.getType());
-
Kotlin:传 KClass 或 KType;或使用 typeOf()(需 inline + reified)
inline fun <reified T> parse(json: String): T
val users: List<User> = parse(json)
4.2 “子类固化”(super type token / 匿名子类捕获)
让匿名子类在“继承点”把类型参数写死;父类的泛型超类签名可被反射取到。
Type t = new TypeReference<List<User>>() {}.getType(); // Jackson
Type t = new TypeToken<Map<String,User>>(){}.getType();// Gson/Guava
4.3 读“声明处签名”
-
类/字段/方法上若写死了类型(如 List 字段),反射能拿到 ParameterizedType;若是 List 只能拿到 TypeVariable,与实例无关。
4.4 Kotlin 的reified(实化类型参数)
- 仅在 inline 函数可用:编译器把 T 的实际类型写入调用点,可在函数体内用 T::class、is T、typeOf()。
- 本质仍是编译期技巧,不是改变 JVM 擦除机制。
5. 擦除带来的典型限制 & 正确写法
| 场景 | 不能做 | 正确姿势 |
|---|---|---|
| 创建 T 或 T[] | new T() / new T[] | 传 Class / 使用 Array.newInstance(componentClass,n);Kotlin 用 reified T + arrayOfNulls(n) |
| 判断具体参数类型 | o instanceof List | o instanceof List<?>;或让调用方传 Type/KType |
| 捕获泛型异常 | catch (E e) where E 是类型变量 | 不支持;用非泛型异常或在类型层分派 |
| 仅靠泛型签名重载 | void f(List) vs void f(List) | 不行(擦除后签名相同);换方法名或加非泛型参数 |
| 可变参数+泛型数组告警 | List... args | 用 @SafeVarargs 或避免创建泛型数组;内部谨慎转换 |
| Kotlin is List | 受擦除限制 | is List<*>;如需元素类型,用 reified 或手工校验元素 |
6. Kotlin 实化的常用模式
// 1) 反序列化
inline fun <reified T> parse(json: String): T = ...
// 2) 过滤指定类型
inline fun <reified T> Iterable<*>.only(): List<T> = filterIsInstance<T>()
// 3) 拿 KType(包含通配/可空等完整信息)
@OptIn(ExperimentalStdlibApi::class)
inline fun <reified T> kt(): KType = typeOf<T>()
注意:reified 只在 inline 函数生效;若把函数存成函数值或跨边界调用,就失去实化效果(因为不再内联)。
7. 与桥方法(bridge)/ 协变返回的关系(简述)
- 擦除会让 compareTo(T) 变成 compareTo(Object);子类实现 compareTo(MyType) 时签名不匹配,编译器会生成 桥方法 适配到擦除后的签名,保证多态正确。这是擦除的副作用之一,但与“拿到真实类型”无关。
8. 实战清单(Java & Kotlin)
Java:Gson 解析泛型集合
Type t = new TypeToken<List<User>>() {}.getType();
List<User> users = gson.fromJson(json, t);
Java:安全创建泛型数组
@SuppressWarnings("unchecked")
static <T> T[] newArray(Class<T> c, int n) {
return (T[]) java.lang.reflect.Array.newInstance(c, n);
}
Kotlin:reified + typeOf
@OptIn(ExperimentalStdlibApi::class)
inline fun <reified T> decode(json: String): T {
val ktype = typeOf<T>() // 完整类型(可空、通配、嵌套参数)
// 交给你的序列化库…
TODO()
}
Kotlin:校验 List 元素类型
inline fun <reified T> Any?.isListOf(): Boolean =
this is List<*> && this.all { it is T }
9. 排坑与最佳实践
- 需要运行时知道具体类型 → 显式传 Type/KType 或用 reified。别指望从实例里“猜”出 T。
- 不要用原始类型(List):会把类型错误推迟到运行时。
- API 设计:涉及泛型反序列化/反射的库一律收“类型令牌” (Java 收 Type,Kotlin 收 KType 或提供 reified 重载)。
- 避免基于泛型的重载:擦除后签名冲突;把差异体现在参数个数/非泛型类型上。
- 数组 vs 集合:数组可反射但协变不安全;多数场景优先用集合 + 通配/投影(? extends/out、? super/in)。
- Kotlin reified 的边界:仅 inline、仅在调用点有效;跨模块 ABI 要保留非 reified重载收 KType 以兼容。
一句话总结
擦除让泛型成为主要的编译期约束,运行时只保留原始类与“声明处签名”;想在运行时拿到“真实类型”,就显式传类型令牌或在 Kotlin 用 inline + reified。其余时候你得到的只是擦除后的上界。设计库/接口时围绕这个现实来:令牌/实化 + 反射签名,就能把“类型安全 + 运行时能力”同时拿到。