关于Java的TypeReference

midoll 353 2023-03-15

关于Java的TypeReference

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

但我们有时候确实需要在运行时获取一定的泛型信息。考虑这样的情况:在一个servlet应用里(为什么是servlet?因为spring mvc遇不到这个问题),我们要求前端使用JSON来发送请求,并规定了请求的格式——

{
  "type": "search",
  "data": "该字段可自定义"
}

为此,对应的POJO为——

@Data
class RequestDto<T> {
    String type;
    T data;
}

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


// ...
String requestBody = getBody(request);
ObjectMapper objectMapper = new ObjectMapper();

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

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


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

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


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

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


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,即——


class RequestDto {
    String type;
    Object data;
}

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

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

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

获取其的demo如下——


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)直接拿到该信息。


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.class了。对上面的json反序列化的代码,我们可以使用TypeReference的匿名实现类而非class来保留泛型信息——


RequestDto<List<Integer>> req = objectMapper.readValue(
    "{\"type\":\"search\", \"data\": \"hello world!\"}", new TypeReference<RequestDto<List<Integer>>>(){});
/*
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"])
	at com.fasterxml.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:59)
	...
*/

我们仍旧会得到一个异常,但这个异常是符合预期的,容易理解的,是在进行反序列化中抛出的!


# java