Java语言的泛型采用的是擦除法实现的伪泛型,泛型信息(类型变量、参数化类型)编译之后通通被除掉了。使用擦除法的好处就是实现简单、非常容易Backport,运行期也能够节省一些类型所占的内存空间。而擦除法的坏处就是,通过这种机制实现的泛型远不如真泛型灵活和强大。Java选取这种方法是一种折中,因为Java最开始的版本是不支持泛型的,为了兼容以前的库而不得不使用擦除法。
验证擦除,我们编写下面代码
1 | public class ErasedTypeEquivalence { |
在例 1 中,我们定义了两个ArrayList数组,不过一个是ArrayList<String>
泛型类型的,只能存储字符串;一个是ArrayList<Integer>
泛型类型的,只能存储整数,最后,我们通过list1对象和list2对象的getClass()方法获取他们的类的信息,最后发现结果为true。说明泛型类型String和Integer都被擦除掉了,只剩下原始类型。
在例 2 中,定义了一个ArrayList泛型类型实例化为Integer对象,如果直接调用add()方法,那么只能存储整数数据,不过当我们利用反射调用add()方法的时候,却可以存储字符串,这说明了Integer泛型实例在编译之后被擦除掉了,只保留了原始类型。
上面两次提到了原始类型,什么是原始类型?原始类型 就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型,无论何时定义一个泛型,相应的原始类型都会被自动提供,类型变量擦除,并使用其限定类型(无限定的变量用Object)替换。
问题案例
最近在搭系统基础代码架构,其中就涉及到系统数据字典 功能,以前都是用varchar类型保存字典内容,这次准备玩点新花样,准备用上MySQL的JSON类型保存字典表的内容字段。实际操作之后就遇到了泛型擦除问题,如下图,我虽然对content字段的List指定了泛型DictContent,但是在做类型转换时,只能指定javaType=List
,没有也不能指定其泛型:
在没有指定泛型的情况下,JacksonTypeHandler
在做类型转换后生成的集合的泛型就与预期的不一致:
原因分析
原因很简单,在resultMap中指定的JavaType是java.util.List
,此处只能指定类类型,并不能指定泛型。而在对应的类型转换类中也没有指定其泛型,而List<DictContent>
和List<Object>
的类类型是一样的,所以在给content字段赋值时是不会报错的。但是一旦你需要操作List的中的元素,在取出元素时,JVM就发现你要的类型是 DictContent
而实际上是LinkedHashMap
,就会抛出类型转换异常。
通俗的讲就是你准备买华为手机(将JSON类型转成List<DictContent>
类型),但是买的时候没有说要买什么牌子的手机(在javaType中只指定了List类型,没有也无法指定泛型),而店子里有很多牌子的手机,所以店家就随便给了你一款手机。
以下是MybatisPlus中的部分源码,可以看到在没有指定List的泛型的情况下,通过JacksonTypeHandler
处理后的元素类型并不是我们预期的类型:
下图我们可以看到JacksonTypeHandler
是BaseTypeHandler
的子类,而且指定了BaseTypeHandler
中的泛型是Object
类型,但是上图中的泛型却是LinkedHashMap
。至于为什么是LinkedHashMap
,我觉得是JVM指定的,如果哪位大佬比较清楚这块的逻辑还请在评论中指点一下,十分感谢!
解决方案
既然原因搞清楚了,解决方案就呼之欲出了,有两种方案:
自定义一个指定泛型的集合类替代List
引用上文中通俗的说法,这个方案就是在买手机的时候告诉卖家,我要买华为手机
自定义一个指定泛型的TypeHandler类替代JacksonTypeHandler类
而这里的的通俗的说法就是让店家只卖华为手机
第一种解决方式
第二种解决方式
替换后结果如下
以上两种方案都可以实现我们的需求。从工作量上来说,自定义一个List
1 | package com.luxsun.platform.lux.kernel.common.handler; |
- Java 方式
1 | …… |
- Xml 方式
1 | <resultMap id="serviceBoMap" type="ServiceBO"> |
至此,泛型擦除问题解决。