Java 中的 TypeReference 的使用方法比较奇怪,想要寻根问底,必须要更加了解 Java 的泛型擦除机制。
我们知道,Java 中的泛型是编译期的,在运行时其会被擦除掉,比如我们编写代码List<Integer> 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<T>
。比如某个接口要求前端发送RequestDto<List<Integer>>
。我们在 servlet 中可能得这么写——
String requestBody = getBody(request);ObjectMapper objectMapper = new ObjectMapper (); RequestDto<List<Integer>> req = objectMapper.readValue(requestBody, RequestDto<List<Integer>>.class);
但这个通不过编译——所谓的RequestDto<List<Integer>>.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);
成了!我们再试试错误的输入?
RequestDto<List<Integer>> req = objectMapper.readValue( "{\"type\":\"search\", \"data\": \"hello world!\"}" , RequestDto.class); req.getData().forEach(System.out::println);
抛异常了!这符合预期,但是却是在req.getData()
时抛的 cast 异常,而非 json 转换时抛出异常。
这是肿么回事呢?从运行时看来,我们是在试图将字符串{"type":"search", "data": "hello world!"}
转换成类型 RequestDto,即——
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()); } }class Bar extends RequestDto <Integer> { public static void main (String[] args) { System.out.printf("父类的泛型类型:%s\n" , Bar.class.getGenericSuperclass()); } }
前者显然为 Spring mvc 所利用——控制器的接口能够正确处理泛型类,而后者则是为所谓的 TypeReference 所利用的——通过继承的方式来保存泛型信息。我们可以通过匿名实现类来在行内(inline)直接拿到该信息。
public static void main (String[] args) { ParameterizedType genericType = (ParameterizedType) new RequestDto <Integer>(){}.getClass().getGenericSuperclass(); System.out.printf("实际类型:%s\n" , genericType); System.out.printf("泛型参数:%s\n" , genericType.getActualTypeArguments()[0 ]); }
这样,我们实际上就能够间接地表示RequestDto<Integer>.class
了。对上面的 json 反序列化的代码,我们可以使用 TypeReference 的匿名实现类而非 class 来保留泛型信息——
RequestDto<List<Integer>> req = objectMapper.readValue( "{\"type\":\"search\", \"data\": \"hello world!\"}" , new TypeReference <RequestDto<List<Integer>>>(){}); req.getData().forEach(System.out::println);
我们仍旧会得到一个异常,但这个异常是符合预期的,容易理解的,是在进行反序列化中抛出的!这说明泛型信息确实得到利用了。