从 JVM 底层看泛型:泛型擦除、Signature 属性与 Super Type Token 模式

目录

[一、为什么 List.class 在语法上就不存在?](#一、为什么 List.class 在语法上就不存在?)

二、泛型擦除到底擦了什么,又保留了什么?

[执行层面:在 java 源文件 -> .class 文件的时候属性被擦除](#执行层面:在 java 源文件 -> .class 文件的时候属性被擦除)

[三、在java源代码编译成 .class 的时候](#三、在java源代码编译成 .class 的时候)

四、元数据层面:Signature属性保留了泛型签名

[五、getClass() 为什么拿不到泛型信息?](#五、getClass() 为什么拿不到泛型信息?)

[六、TypeToken 的核心:Super Type Token](#六、TypeToken 的核心:Super Type Token)

[七、Gson 的 TypeToken 实现中,关键逻辑就在无参构造器里:](#七、Gson 的 TypeToken 实现中,关键逻辑就在无参构造器里:)



一、为什么 List<Demo>.class 在语法上就不存在?

在 Java里,.class 是一个类字面量,它唯一合法的操作数是原始类型或数组类型的编译期名称。List<Demo> 是一个参数化类型,它在 Java 语法中不是一个可以独立求值的 "类型名字",所以 List<Demo>.class 在编译器看来就是一个非法的表达式,直接报错。

更根本的原因在于:JVM 在运行时根本不跟踪泛型类实例的类型参数。 new ArrayList<String>() 在运行时就是一个 ArrayList 实例,JVM 不会在对象头或 Class 元数据里记录"这个实例的E是 String"。既然运行时不存在一个代表 List<Demo> 的实体,自然也就不可能有一个对应的 .class 对象。


二、泛型擦除到底擦了什么,又保留了什么?

执行层面:在 java 源文件 -> .class 文件的时候属性被擦除

擦除的规则:类型变量擦除到最左边界(没有显式边界,就擦到Object),参数化类型擦除到原始类型。这意味着方法内部的字节码指令 、字段描述符 、方法描述符中,不再出现 <Demo> 这样的类型参数。

  • 无边界泛型 <T> 擦除 → Object
java 复制代码
public <T> void test(T t) {}
// 擦除后
public void test(Object t) {}
  • 有上边界 <T extends Number> 擦除 → Number
java 复制代码
public <T extends Number> void test(T t){}
//擦除后
public void test(Number t){}
  • 多个边界 <T extends Serializable & Comparable<T>> 最左边界是 Serializable,擦除到 Serializable
java 复制代码
<T extends Serializable & Comparable<T>>
//擦除:Serializable

三、在java源代码编译成 .class 的时候

java 复制代码
List<Demo> list = new ArrayList<>();
Demo d = list.get(0);

这个代码逻辑会变成

java 复制代码
List list = new ArrayList();
Demo d = (Demo) list.get(0);

强转是编译器在擦除后为了保证类型正确性而插入的。


四、元数据层面:Signature属性保留了泛型签名

但 Class 文件里面还有一个 Signature 属性。对于任何自身是泛型的、或者签名中包含了类型变量或参数化类型实例的类、字段、方法,javac 都会发出 Signature 属性,把未擦除的泛型签名记录在元数据中。

所以更精准的表述时:擦除发生在 java源文件 编译成 .class 可执行文件的时候,而泛型签名被"归档"到了 Signature 属性里。 运行时直接问 JVM "这个 List 实例的泛型参数是什么",JVM 回答不上来,但是如果去看声明(字段声明、方法声明、类的父类声明),签名还在。

五、getClass() 为什么拿不到泛型信息?

list.getClass() 返回的是 class java.util.ArrayList

java 复制代码
List<Demo> list = new ArrayList<>();
System.out.println(list.getClass());

打印的是

List.class 永远代表 List 本身,所有带有不同泛型参数的 List,运行时共用同一个 Class 对象

list1.getClass() 返回的是 ArrayList.class ,list2.getClass() 返回的也是 ArrayList.class

java 复制代码
List<Demo> list1 = new ArrayList<>();
List<String> list2 = new ArrayList<>();
System.out.println(list1.getClass() == list2.getClass()); // true
// 两者getClass拿到的都是 ArrayList.class,不是带泛型的

getClass() 是 Object 的方法,它反映的是实例的运行时类。JVM 不跟踪泛型类实例的类型参数,所以 List<Demo> 和 List<String> 的实例,getClass() 都返回同一个 ArrayList.class ,比较结果为true。

注意:这里用的是 ArrayList 而不是 List,因为运行时对象是 ArrayList 的实例。这恰好说明 getClass() 反映的是实现类,而不是声明类型,它和泛型参数没有任何关系。

六、TypeToken 的核心:Super Type Token

既然实例 不携带泛型参数,那有没有什么东西是JVM会跟踪泛型参数的?

有:泛型类的子类声明。

JVM虽然不跟踪 new ArrayList<String>() 的泛型参数,但如果有一个类显式继承了一个参数化类型,比如 class StringList extends ArrayList<String>,JVM会在 StringList 的Class元数据中记录"它的直接超类是 ArrayList<String> "。这个记录位于 Signature 属性中,可以通过Class.getGenericSuperclass() 读到。

Neal Gafter 的 Super Type Token 模式正是利用这一点:匿名内部类会生成一个真实的Class文件,它的直接超类是 TypeToekn<List<Demo>>,这个参数化超类的信息被写进了匿名类的 Signature 属性中。然后在 TypeToken 的构造器里调用 getClass().getGenericSuperclass(),就拿到 ParameterizedType,从而取出 List<Demo> 这个完整的 Type。

java 复制代码
public class TypeToken<T> {
  private final Class<? super T> rawType;
  private final Type type;
  private final int hashCode;


  @SuppressWarnings("unchecked")
  protected TypeToken() {
    this.type = getTypeTokenTypeArgument();
    this.rawType = (Class<? super T>) $Gson$Types.getRawType(type);
    this.hashCode = type.hashCode();
  }
 private Type getTypeTokenTypeArgument() {
    Type superclass = getClass().getGenericSuperclass();
    if (superclass instanceof ParameterizedType) {
      ParameterizedType parameterized = (ParameterizedType) superclass;
      if (parameterized.getRawType() == TypeToken.class) {
        return $Gson$Types.canonicalize(parameterized.getActualTypeArguments()[0]);
      }
    }
    // Check for raw TypeToken as superclass
    else if (superclass == TypeToken.class) {
      throw new IllegalStateException("TypeToken must be created with a type argument: new TypeToken<...>() {}; "
          + "When using code shrinkers (ProGuard, R8, ...) make sure that generic signatures are preserved.");
    }

    // User created subclass of subclass of TypeToken
    throw new IllegalStateException("Must only create direct subclasses of TypeToken");
  }
}

七、Gson 的 TypeToken 实现中,关键逻辑就在无参构造器里:

java 复制代码
protected TypeToken() {
    this.type = getTypeTokenTypeArgument();
    ...
}
private Type getTypeTokenTypeArgument() {
    Type superclass = getClass().getGenericSuperclass();
    if (superclass instanceof ParameterizedType) {
        return ((ParameterizedType) superclass.getActualTypeArguments()[0];
    }
    ...
}

new TypeToken<List<Demo>>(){} 这行代码编译时生成一个匿名子类 ,这个子类的父类泛型 TypeToken<List<Demo>> 会被存入子类 class 文件的 Signature 属性。匿名子类的名字为:Main$1,它的 .class 文件里面的属性字段的第一个就是 Signature 属性。这时候 superclass.getActualTypeArguments()0 字段就指向了 Signature。

运行时,在 TypeToken 无参构造器里:

getClass() → 获取匿名子类的 Class 对象,调用 getGenericSuperclass() ,读取子类的 Signature,拿到父类上写的泛型参数 List<Demo>.

它之所以强制用户写 new TypeToken<List<Demo>>(){} ,是为了确保 getClass() 返回的是一个直接继承了 TypeToken<List<Demo>> 的匿名子类,而不是 TypeToken 本身。

相关推荐
FLJwu4 小时前
运动相机图像传感器选型实战|第二篇:参数看着一模一样,为什么骑行录像疯狂果冻、拖影翻车?
android·c语言·开发语言·数码相机·运动相机·oem 量产
菜鸟小杰子4 小时前
手动修改 Spring Boot 嵌套 JAR 包:大包小包隔离打包实战
java·jar
Escalating_xu4 小时前
【C语言项目实战】贪吃蛇(下):移动、碰撞与计分,完成一局可玩的游戏
c语言·开发语言·游戏
计算机编程-吉哥4 小时前
计算机毕业设计 基于 SpringBoot + Vue 的剧本杀系统 Java毕业设计【任务书、开题报告、中期报告、论文、源码】
java·vue.js·spring boot·毕业设计·课程设计·计算机毕业设计选题
code2cat4 小时前
【随笔】RAG语义缓存:问题很相似,答案就能直接复用吗
java·缓存·知识库·rag
Rain的Java大神之路5 小时前
面了 50 个候选人,90% 都挂在 LeetCode 3 无重复字符的最长子串:滑动窗口的 5 个致命坑
java·后端·leetcode
西凉的悲伤5 小时前
Jsoup 库 HTML 解析详解
java·前端·html·jsoup
qq_401700415 小时前
Qt Modbus 源码剖析:从 QModbusClient 到工业级通信框架
开发语言·qt·系统架构
星夜离尘565 小时前
MATLAB 角度解缠遇到丢样:先分段,再说明圈数为什么不可恢复
开发语言·matlab