本文正在参与「金石计划 . 瓜分6万现金大奖」
哈喽咱们好啊,我是Hydra~ 在前面的文章中,咱们讲过Java中泛型的类型擦除,不过有小伙伴在后台留言提出了一个问题,带有泛型的实体的反序列化进程是如何实现的,今天咱们就来看看这个问题。
铺垫
咱们挑选fastjson
来进行反序列化的测试,在测试前先界说一个实体类:
@Data
public class Foo<T> {
private String val;
private T obj;
}
假如咱们对泛型的类型擦除比较熟悉的话,就会知道在编译完成后,其实在类中是没有泛型的。咱们仍是用Jad
反编译一下字节码文件,能够看到没有类型限制的T
会被直接替换为Object
类型:
下面运用fastjson
进行反序列化,先不指定Foo
中泛型的类型:
public static void main(String[] args) {
String jsonStr = "{\"obj\":{\"name\":\"Hydra\",\"age\":\"18\"},\"val\":\"str\"}";
Foo<?> foo = JSONObject.parseObject(jsonStr, Foo.class);
System.out.println(foo.toString());
System.out.println(foo.getObj().getClass());
}
查看履行成果,很明显fastjson
不知道要把obj
里的内容反序列化成咱们自界说的User
类型,于是将它解析成了JSONObject
类型的目标。
Foo(val=str, obj={"name":"Hydra","age":"18"})
class com.alibaba.fastjson.JSONObject
那么,假如想把obj
的内容映射为User
实体目标应该怎样写呢?下面先来演示几种过错写法。
过错写法1
尝试在反序列化时,直接指定Foo
中的泛型为User
:
Foo<User> foo = JSONObject.parseObject(jsonStr, Foo.class);
System.out.println(foo.toString());
System.out.println(foo.getObj().getClass());
成果会报类型转化的过错,JSONObject
不能转成咱们自界说的User
:
Exception in thread "main" java.lang.ClassCastException: com.alibaba.fastjson.JSONObject cannot be cast to com.hydra.json.model.User
at com.hydra.json.generic.Test1.main(Test1.java:24)
过错写法2
再试试运用强制类型转化:
Foo<?> foo =(Foo<User>) JSONObject.parseObject(jsonStr, Foo.class);
System.out.println(foo.toString());
System.out.println(foo.getObj().getClass());
履行成果如下,能够看到,泛型的强制类型转化尽管不会报错,可是相同也没有生效。
Foo(val=str, obj={"name":"Hydra","age":"18"})
class com.alibaba.fastjson.JSONObject
好了,现在请咱们忘掉上面这两种过错的运用办法,代码中千万别这么写,下面咱们看正确的写法。
正确写法
在运用fastjson
时,能够凭借TypeReference
完成指定泛型的反序列化:
public class TypeRefTest {
public static void main(String[] args) {
String jsonStr = "{\"obj\":{\"name\":\"Hydra\",\"age\":\"18\"},\"val\":\"str\"}";
Foo foo2 = JSONObject.parseObject(jsonStr, new TypeReference<Foo<User>>(){});
System.out.println(foo2.toString());
System.out.println(foo2.getObj().getClass());
}
}
运转成果:
Foo(val=str, obj=User(name=Hydra, age=18))
class com.hydra.json.model.User
Foo
中的obj
类型为User
,符合咱们的预期。下面咱们就看看,fastjson
是如何凭借TypeReference
完成的泛型类型擦除后的复原。
TypeReference
回头再看一眼上面的代码中的这句:
Foo foo2 = JSONObject.parseObject(jsonStr, new TypeReference<Foo<User>>(){});
重点是parseObject
办法中的第二个参数,注意在TypeReference<Foo<User>>()
有一对大括号{}
。也便是说这儿创建了一个承继了TypeReference
的匿名类的目标,在编译完成后的项目target
目录下,能够找到一个TypeRefTest$1.class
字节码文件,由于匿名类的命名规矩便是主类名+$+(1,2,3……)
。
反编译这个文件能够看到这个承继了TypeReference
的子类:
static class TypeRefTest$1 extends TypeReference
{
TypeRefTest$1()
{
}
}
咱们知道,在创建子类的目标时,子类会默许先调用父类的无参结构办法,所以看一下TypeReference
的结构办法:
protected TypeReference(){
Type superClass = getClass().getGenericSuperclass();
Type type = ((ParameterizedType) superClass).getActualTypeArguments()[0];
Type cachedType = classTypeCache.get(type);
if (cachedType == null) {
classTypeCache.putIfAbsent(type, type);
cachedType = classTypeCache.get(type);
}
this.type = cachedType;
}
其实重点也便是前两行代码,先看榜首行:
Type superClass = getClass().getGenericSuperclass();
尽管这儿是在父类中履行的代码,可是getClass()
得到的一定是子类的Class目标,由于getClass
()办法获取到的是当时运转的实例自身的Class,不会由于调用位置改变,所以getClass()
得到的一定是TypeRefTest$1
。
获取当时目标的Class后,再履行了getGenericSuperclass()
办法,这个办法与getSuperclass
类似,都会回来直接承继的父类。不同的是getSuperclas
没有回来泛型参数,而getGenericSuperclass
则回来了包含了泛型参数的父类。
再看第二行代码:
Type type = ((ParameterizedType) superClass).getActualTypeArguments()[0];
首要将上一步取得的Type
强制类型转化为ParameterizedType
参数化类型,它是泛型的一个接口,实例则是承继了它的ParameterizedTypeImpl
类的目标。
在ParameterizedType
中界说了三个办法,上面代码中调用的getActualTypeArguments()
办法就用来回来泛型类型的数组,可能回来有多个泛型,这儿的[0]
便是取出了数组中的榜首个元素。
验证
好了,理解了上面的代码的作用后,让咱们通过debug来验证一下上面的进程,履行上面TypeRefTest
的代码,查看断点中的数据:
这儿发现一点问题,按照咱们上面的分析,讲道理这儿父类TypeReference
的泛型应该是Foo<User>
啊,为什么会呈现一个List<String>
?
别着急,让咱们接着往下看,假如你在TypeReference
的无参结构办法中加了断点,就会发现代码履行中会再调用一次这个结构办法。
好了,这次的成果和咱们的预期相同,父类的泛型数组中存储了Foo<User>
,也便是说其实TypeRefTest$1
承继的父类,完成的来说应该是TypeReference<Foo<User>>
,可是咱们上面反编译的文件中由于擦除的原因没有显现。
那么还有一个问题,为什么这个结构办法会被调用了两次呢?
看完了TypeReference
的代码,终于在代码的最终一行让我发现了原因,原来是在这儿先创建了一个TypeReference
匿名类目标!
public final static Type LIST_STRING
= new TypeReference<List<String>>() {}.getType();
因而整段代码履行的顺序是这样的:
- 先履行父类中静态成员变量的界说,在这儿声明并实例化了这个
LIST_STRING
,所以会履行一次TypeReference()
结构办法,这个进程对应上面的榜首张图 - 然后在实例化子类的目标时,会再履行一次父类的结构办法
TypeReference()
,对应上面的第二张图 - 最终履行子类的空结构办法,什么都没有干
至于在这儿声明的LIST_STRING
,在其他当地也没有被再运用过,Hydra也不知道这行代码的含义是什么,有理解的小伙伴能够留言告诉我。
这儿在拿到了Foo
中的泛型User
后,后边就能够按照这个类型来反序列化了,对后续流程有爱好的小伙伴能够自己去啃啃源码,这儿就不展开了。
扩展
了解了上面的进程后,咱们最终通过一个比如加深一下理解,以常用的HashMap
作为比如:
public static void main(String[] args) {
HashMap<String,Integer> map=new HashMap<String,Integer>();
System.out.println(map.getClass().getSuperclass());
System.out.println(map.getClass().getGenericSuperclass());
Type[] types = ((ParameterizedType) map.getClass().getGenericSuperclass())
.getActualTypeArguments();
for (Type t : types) {
System.out.println(t);
}
}
履行成果如下,能够看到这儿取到的父类是HashMap
的父类AbstractMap
,并且取不到实际的泛型类型。
class java.util.AbstractMap
java.util.AbstractMap<K, V>
K
V
修正上面的代码,仅做一点小改动:
public static void main(String[] args) {
HashMap<String,Integer> map=new HashMap<String,Integer>(){};
System.out.println(map.getClass().getSuperclass());
System.out.println(map.getClass().getGenericSuperclass());
Type[] types = ((ParameterizedType) map.getClass().getGenericSuperclass())
.getActualTypeArguments();
for (Type t : types) {
System.out.println(t);
}
}
履行成果大有不同,能够看到,只是在new HashMap<String,Integer>()
的后边加了一对大括号{}
,就能够取到泛型的类型了:
class java.util.HashMap
java.util.HashMap<java.lang.String, java.lang.Integer>
class java.lang.String
class java.lang.Integer
由于这儿实例化的是一个承继了HashMap
的匿名内部类的目标,因而取到的父类便是HashMap
,并能够获取到父类的泛型类型。
其实也能够再换一个写法,把这个匿名内部类换成显现声明的非匿名的内部类,再修正一下上面的代码:
public class MapTest3 {
static class MyMap extends HashMap<String,Integer>{}
public static void main(String[] args) {
MyMap myMap=new MyMap();
System.out.println(myMap.getClass().getSuperclass());
System.out.println(myMap.getClass().getGenericSuperclass());
Type[] types = ((ParameterizedType) myMap.getClass().getGenericSuperclass())
.getActualTypeArguments();
for (Type t : types) {
System.out.println(t);
}
}
}
运转成果与上面完全相同:
class java.util.HashMap
java.util.HashMap<java.lang.String, java.lang.Integer>
class java.lang.String
class java.lang.Integer
仅有不同的是显式生成的内部类与匿名类命名规矩不同,这儿生成的字节码文件不是MapTest3$1.class
,而是MapTest3$MyMap.class
,在$
符后边运用的是咱们界说的类名。
好啦,那么这次的填坑之旅就到这儿,我是Hydra,下期见。
作者简介,
码农参上
,一个热爱共享的公众号,有趣、深化、直接,与你聊聊技能。欢迎增加好友,进一步沟通。