擦除(type erasure)与可重ification

35 阅读5分钟

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 Listo 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. 排坑与最佳实践

  1. 需要运行时知道具体类型显式传 Type/KType 或用 reified。别指望从实例里“猜”出 T。
  2. 不要用原始类型(List):会把类型错误推迟到运行时。
  3. API 设计:涉及泛型反序列化/反射的库一律收“类型令牌” (Java 收 Type,Kotlin 收 KType 或提供 reified 重载)。
  4. 避免基于泛型的重载:擦除后签名冲突;把差异体现在参数个数/非泛型类型上。
  5. 数组 vs 集合:数组可反射但协变不安全;多数场景优先用集合 + 通配/投影(? extends/out、? super/in)。
  6. Kotlin reified 的边界:仅 inline、仅在调用点有效;跨模块 ABI 要保留非 reified重载收 KType 以兼容。

一句话总结

擦除让泛型成为主要的编译期约束,运行时只保留原始类与“声明处签名”;想在运行时拿到“真实类型”,就显式传类型令牌或在 Kotlin 用 inline + reified。其余时候你得到的只是擦除后的上界。设计库/接口时围绕这个现实来:令牌/实化 + 反射签名,就能把“类型安全 + 运行时能力”同时拿到。