关于 TypeReference

Java 中的 TypeReference 的使用方法比较奇怪,想要寻根问底,必须要更加了解 Java 的泛型擦除机制。

我们知道,Java 中的泛型是编译期的,在运行时其会被擦除掉,比如我们编写代码List<Integer> lst = new ArrayList<>();,从运行时看来将会是List lst = new ArrayList();,只留下了原始类型(raw type)。

但考虑这样的情况:在一个 servlet 应用里(为什么是 servlet?因为 spring mvc 遇不到这个问题),我们要求前端使用 JSON 来发送请求,并规定了请求的格式——

1
2
3
4
{
"type" :"search",
"data" : "该字段可自定义"
}

为此,对应的 POJO 为——

1
2
3
4
5
@Data
class RequestDto<T> {
String type;
T data;
}

在 servlet 中,我们需要将请求体字符串转换为特定的RequestDto<T>。比如某个接口要求前端发送RequestDto<List<Integer>>。我们在 servlet 中可能得这么写——

1
2
3
4
5
6
7
// ...
String requestBody = getBody(request);
ObjectMapper objectMapper = new ObjectMapper();

// 转换 string 为相应对象
RequestDto<List<Integer>> req = objectMapper.readValue(requestBody, RequestDto<List<Integer>>.class);
// ...

但这个通不过编译——所谓的RequestDto<List<Integer>>.class是不存在的,因为在运行时不存在泛型类型,我们只能得到RequestDto.class,所以只能这么写——

1
RequestDto<List<Integer>> req = objectMapper.readValue(requestBody, RequestDto.class);

虽然有个恼火的警告,但至少能编译了。我们整个 demo 试试——

1
2
3
4
5
6
7
8
RequestDto<List<Integer>> req = objectMapper.readValue(
"{\"type\": \"search\", \"data\": [1, 2, 3]}", RequestDto.class);
req.getData().forEach(System.out::println);
/*
1
2
3
*/

成了!我们再试试错误的输入?

1
2
3
4
5
6
7
RequestDto<List<Integer>> req = objectMapper.readValue(
"{\"type\":\"search\", \"data\": \"hello world!\"}", RequestDto.class);
req.getData().forEach(System.out::println);
/*
Exception in thread "main" java.lang.ClassCastException: class java.lang.String cannot be cast to class java.util.List (java.lang.String and java.util.List are in module java.base of loader 'bootstrap')
at com.optimagrowth.license.LicenseServiceApplication.main(LicenseServiceApplication.java:41)
*/

抛异常了!这符合预期,但是却是在req.getData()时抛的 cast 异常,而非 json 转换时抛出异常。

这是肿么回事呢?从运行时看来,我们是在试图将字符串{"type":"search", "data": "hello world!"}转换成类型 RequestDto,即——

1
2
3
4
class RequestDto {
String type;
Object data;
}

这河里吗?可太合理了,既然是Object,那是任何类型都是可以的了。但这显然是不符合我们的需要的——如果类型的错误必须要在我们使用的时候才能暴露出来,那这和动态类型语言何异?

问题就出在 Java 的泛型擦除机制。我们有什么手段来规避它吗?库函数的设计者告诉我们,有!

Java 的泛型擦除机制实际上至少在两个地方没有擦掉——方法的参数和返回值;继承泛型类的类。

获取其的 demo 如下——

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Foo {
List<List<Integer>> someMethod(List<Boolean> lst) { return null; }
public static void main(String[] args) {
Method method = Foo.class.getDeclaredMethods()[0];
System.out.printf("方法参数:%s\n", method.getGenericParameterTypes()[0]);
System.out.printf("方法返回值:%s\n",method.getGenericReturnType());
}
}
/*
方法参数:java.util.List<java.lang.Boolean>
方法返回值:java.util.List<java.util.List<java.lang.Integer>>
*/

class Bar extends RequestDto<Integer> {
public static void main(String[] args) {
System.out.printf("父类的泛型类型:%s\n", Bar.class.getGenericSuperclass());
}
}
/*
父类的泛型类型:me.ykn.RequestDto<java.lang.Integer>
*/

前者显然为 Spring mvc 所利用——控制器的接口能够正确处理泛型类,而后者则是为所谓的 TypeReference 所利用的——通过继承的方式来保存泛型信息。我们可以通过匿名实现类来在行内(inline)直接拿到该信息。

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
// 这里向下转型是为 demo 展示需要,实际使用时一般只需要使用 Type 类型即可
ParameterizedType genericType = (ParameterizedType) new RequestDto<Integer>(){}.getClass().getGenericSuperclass();
System.out.printf("实际类型:%s\n", genericType);
System.out.printf("泛型参数:%s\n", genericType.getActualTypeArguments()[0]);
}
/*
实际类型:me.ykn.RequestDto<java.lang.Integer>
泛型参数:class java.lang.Integer
*/

这样,我们实际上就能够间接地表示RequestDto<Integer>.class了。对上面的 json 反序列化的代码,我们可以使用 TypeReference 的匿名实现类而非 class 来保留泛型信息——

1
2
3
4
5
6
7
8
9
RequestDto<List<Integer>> req = objectMapper.readValue(
"{\"type\":\"search\", \"data\": \"hello world!\"}", new TypeReference<RequestDto<List<Integer>>>(){});
req.getData().forEach(System.out::println);

/*
Exception in thread "main" com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize instance of `java.util.ArrayList<java.lang.Integer>` out of VALUE_STRING token
at [Source: (String)"{"type":"search", "data": "hello world!"}"; line: 1, column: 27] (through reference chain: com.optimagrowth.license.RequestDto["data"])
...
*/

我们仍旧会得到一个异常,但这个异常是符合预期的,容易理解的,是在进行反序列化中抛出的!这说明泛型信息确实得到利用了。