1. 概述

在 Java 泛型编程中,我们经常面临一个经典问题:如何在运行时保留泛型类型信息?这篇文章带你深入理解 Super Type Token 技术,它是解决类型擦除问题的优雅方案之一。

你可能已经知道,像 Jackson 这样的 JSON 序列化库需要知道目标类型才能正确反序列化数据。对于普通类型,这很简单:

byte[] data = // 从某处获取 JSON 字节流
String json = objectMapper.readValue(data, String.class);

我们通过 String.class 这种“类字面量”告诉 Jackson 我们期望的结果类型。✅

但如果你想要反序列化成一个泛型类型,比如 Map<String, String>,你会发现这条路走不通:

Map<String, String> json = objectMapper.readValue(data, Map<String, String>.class); // ❌ 编译失败!

为什么?这就引出了 Java 泛型的核心痛点 —— 类型擦除。


2. 类型擦除(Type Erasure)

Java 的泛型是“编译期语法糖”,在编译过程中,泛型类型参数会被擦除,只保留原始类型(raw type)。这意味着:

泛型信息仅存在于源码中,运行时无法直接获取完整的泛型类型。

所以 Map<String, String>.class 根本不是一个合法的表达式,因为 Map<String, String> 并不是一个真正的运行时类型。

2.1 什么是类型具象化(Reification)

在编程语言理论中,如果一个类型在运行时仍然可用,我们就说它是 reified(具象化的)

Java 中以下类型是具象化的(reified),可以在运行时访问:

  • 基本类型:如 longint
  • 非泛型类:如 StringRunnable
  • 原始类型(raw types):如 ListHashMap
  • 所有类型参数为无界通配符的泛型:如 List<?>HashMap<?, ?>
  • 上述具象化类型的数组:如 String[]int[]List<?>[]

⚠️ 但像 Map<String, Integer> 这样的具体泛型类型不是具象化的,因此不能写成 .class 形式。

这就导致了一个问题:我们如何向方法传递完整的泛型类型信息?


3. Super Type Token:绕过类型擦除的黑科技

答案是:利用 Java 的一个特性 —— 匿名内部类会保留其父类或接口的泛型信息到字节码中

我们可以创建一个抽象类,通过子类的泛型参数来“捕获”类型信息。这就是所谓的 Super Type Token 模式。

核心实现

public abstract class TypeReference<T> {

    private final Type type;

    public TypeReference() {
        Type superclass = getClass().getGenericSuperclass();
        type = ((ParameterizedType) superclass).getActualTypeArguments()[0];
    }

    public Type getType() {
        return type;
    }
}

关键点解析

  • getClass().getGenericSuperclass():获取当前实例的直接父类的泛型类型(注意是 getGenericSuperclass,不是 getSuperclass
  • ✅ 构造函数中调用该方法,是因为匿名内部类的泛型信息在实例化时仍然存在
  • ✅ 抽象类确保用户必须通过子类(通常是匿名类)来使用它

使用示例

TypeReference<Map<String, Integer>> token = new TypeReference<Map<String, Integer>>() {};
Type type = token.getType();

assertEquals("java.util.Map<java.lang.String, java.lang.Integer>", type.getTypeName());

Type[] typeArguments = ((ParameterizedType) type).getActualTypeArguments();
assertEquals("java.lang.String", typeArguments[0].getTypeName());
assertEquals("java.lang.Integer", typeArguments[1].getTypeName());

⚠️ 注意那个空的 {} —— 它创建了一个匿名子类实例,正是这个“类”的存在让泛型信息得以保留!

实际应用场景

这个技巧不是理论玩具,而是被广泛用于主流框架中:

  • JacksonobjectMapper.readValue(data, new TypeReference<Map<String, String>>() {});
  • Spring FrameworkRestTemplateWebClient 中的泛型类型处理
  • GsonTypeToken 类就是 Super Type Token 的典型实现

✅ 简单粗暴地说:当你需要把泛型类型传给一个方法时,用匿名内部类包装一下就解决了。


4. 总结

  • ❌ Java 泛型存在类型擦除,无法直接获取 Map<String, Integer>.class
  • ✅ 利用匿名内部类 + getGenericSuperclass() 可以在运行时恢复泛型信息
  • ✅ 这种模式被称为 Super Type Token
  • ✅ 主流库如 Jackson、Spring、Gson 都内置了类似实现
  • 💡 踩坑提示:不要忘了 new TypeReference<YourType>() {} 后面的大括号,少了它就只是普通实例化,泛型信息照样丢失!

所有示例代码均可在 GitHub 上找到:
https://github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-lang-oop-generics


原始标题:Super Type Tokens in Java Generics | Baeldung