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),可以在运行时访问:
- 基本类型:如
long
、int
- 非泛型类:如
String
、Runnable
- 原始类型(raw types):如
List
、HashMap
- 所有类型参数为无界通配符的泛型:如
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());
⚠️ 注意那个空的 {}
—— 它创建了一个匿名子类实例,正是这个“类”的存在让泛型信息得以保留!
实际应用场景
这个技巧不是理论玩具,而是被广泛用于主流框架中:
- Jackson:
objectMapper.readValue(data, new TypeReference<Map<String, String>>() {});
- Spring Framework:
RestTemplate
和WebClient
中的泛型类型处理 - Gson:
TypeToken
类就是 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