关于Java的TypeReference
我们知道,Java中的泛型是编译期的,在运行时其会被擦除掉,比如我们编写代码List
但我们有时候确实需要在运行时获取一定的泛型信息。考虑这样的情况:在一个servlet应用里(为什么是servlet?因为spring mvc遇不到这个问题),我们要求前端使用JSON来发送请求,并规定了请求的格式——
{
"type": "search",
"data": "该字段可自定义"
}
为此,对应的POJO为——
@Data
class RequestDto<T> {
String type;
T data;
}
在servlet中,我们需要将请求体字符串转换为特定的RequestDto
// ...
String requestBody = getBody(request);
ObjectMapper objectMapper = new ObjectMapper();
// 转换string为相应对象
RequestDto<List<Integer>> req = objectMapper.readValue(requestBody, RequestDto<List<Integer>>.class);
// ...
但这个通不过编译——所谓的RequestDto<List
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
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)
...
*/
我们仍旧会得到一个异常,但这个异常是符合预期的,容易理解的,是在进行反序列化中抛出的!