本文正在参与「金石计划 . 瓜分6万现金大奖」


哈喽咱们好啊,我是Hydra~ 在前面的文章中,咱们讲过Java中泛型的类型擦除,不过有小伙伴在后台留言提出了一个问题,带有泛型的实体的反序列化进程是如何实现的,今天咱们就来看看这个问题。

铺垫

咱们挑选fastjson来进行反序列化的测试,在测试前先界说一个实体类:

@Data
public class Foo<T> {
    private String val;
    private T obj;
}

假如咱们对泛型的类型擦除比较熟悉的话,就会知道在编译完成后,其实在类中是没有泛型的。咱们仍是用Jad反编译一下字节码文件,能够看到没有类型限制的T会被直接替换为Object类型:

泛型的类型擦除后,fastjson反序列化时如何还原?

下面运用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的代码,查看断点中的数据:

泛型的类型擦除后,fastjson反序列化时如何还原?

这儿发现一点问题,按照咱们上面的分析,讲道理这儿父类TypeReference的泛型应该是Foo<User>啊,为什么会呈现一个List<String>

别着急,让咱们接着往下看,假如你在TypeReference的无参结构办法中加了断点,就会发现代码履行中会再调用一次这个结构办法。

泛型的类型擦除后,fastjson反序列化时如何还原?

好了,这次的成果和咱们的预期相同,父类的泛型数组中存储了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,下期见。

作者简介,码农参上,一个热爱共享的公众号,有趣、深化、直接,与你聊聊技能。欢迎增加好友,进一步沟通。