原创:微信公众号 码农参上,欢迎分享,转载请保留出处。

哈喽大家好啊,我是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,下期见。

作者简介,码农参上,一个热爱分享的公众号,有趣、深入、直接,与你聊聊技术。个人微信DrHydra9,欢迎添加好友,进一步交流。

泛型的类型擦除后,fastjson反序列化时如何还原?的更多相关文章

  1. Java泛型-类型擦除

    一.概述 Java泛型在使用过程有诸多的问题,如不存在List<String>.class, List<Integer>不能赋值给List<Number>(不可协变 ...

  2. Java泛型之类型擦除

    类型擦除 学过C++模板的,在使用Java泛型的时候,会感觉到有点不疑问,例如:(1)无法定义一个泛型数组.无法调用泛型参数对象中对应的方法(当然,通过extends关键字是可以做到,只是比较麻烦): ...

  3. java 泛型的类型擦除和桥方法

    oracle原文地址:https://docs.oracle.com/javase/tutorial/java/generics/erasure.html 在Java中,泛型的引入是为了在编译时提供强 ...

  4. 泛型 Generic 类型擦除引起的问题及解决方法

    参考:http://blog.csdn.net/lonelyroamer/article/details/7868820#comments 因为种种原因,Java不能实现真正的泛型,只能使用类型擦除来 ...

  5. Java泛型:类型擦除

    类型擦除 代码片段一 Class c1 = new ArrayList<Integer>().getClass(); Class c2 = new ArrayList<String& ...

  6. JAVA 泛型之类型擦除

    ★ 泛型是 JDK 1.5 版本引进的概念,之前是没有泛型的概念的,但泛型代码能够很好地和之前版本的代码很好地兼容. CollectionTest.java ---编译成CollectionTest. ...

  7. [改善Java代码]Java的泛型是类型擦除的

    泛型可以减少强制类型的转换,可规范集合的元素类型,还可以提高代码的安全性和可读性,正是因为有了这些优点,自从Java引入泛型之后,项目的编码规则上便多了一条,优先使用泛型. Java泛型(Generi ...

  8. 转:有关Java泛型的类型擦除(type erasing)

    转载自:拈花微笑 自从Java 5引入泛型之后,Java与C++对于泛型不同的实现的优劣便一直是饭后的谈资.在我之前的很多training中,当讲到Java泛型时总是会和C++的实现比较,一般得出的结 ...

  9. java之集合类框架的简要知识点:泛型的类型擦除

    这里想说一下在集合框架前需要理解的小知识点,也是个人的肤浅理解,不知道理解的正不正确,请大家多多指教.这里必须谈一下java的泛型,因为它们联系紧密,我们先看一下这几行代码: Class c1 = n ...

随机推荐

  1. [USACO18DEC]Sort It Out P

    初看本题毫无思路,只能从特殊的 \(K = 1\) 出发. 但是直接考虑构造一组字典序最小的方案还是不好构造,可以考虑先手玩一下样例.通过自己手玩的样例可以发现,貌似没有被选出来的数在原排列中都是递增 ...

  2. curl: (6) Could not resolve host: mirrors.163.com; Unknown error 服务器上解析不了域名,换成ip可以

    原因是DNS域名解析问题: 添加nameserver即可解决 echo nameserver 8.8.8.8 > /etc/resolv.conf 解释一下DNS服务 DNS(Domain Na ...

  3. JAVA变量初始化赋值问题

    感谢大佬:https://www.cnblogs.com/znsongshu/p/6282672.html 在Java中,null值表示引用不指向任何对象.运行过程中系统发现使用了这样一个引用时·可以 ...

  4. centOS 强制卸载PHP

    centOS上的php过低是需要重新安装时,不得不卸载自定义安装,如下操作 查看php版本命令: #php -v 这个命令是删除不干净的 #yum remove php 因为使用这个命令以后再用 #p ...

  5. JAVA初学--Servlet详解

    一.什么是servlet? 处理请求和发送响应的过程是由一种叫做Servlet的程序来完成的,并且Servlet是为了解决实现动态页面而衍生的东西.理解这个的前提是了解一些http协议的东西,并且知道 ...

  6. Python—字符串常用函数

    Python-字符串常用字符串 字符串是一种表示文本的数据类型,使用单引号和双引号及三引号表示 访问字符串中的值字符串的每个字符都对应一个下标,下标编号是从0开始 转义字符字符串的格式化输出切片常用函 ...

  7. docker错误处理——docker Job for docker.service failed because the control process exited with error code.

    (15条消息) docker Job for docker.service failed because the control process exited with error code._Hel ...

  8. 微服务与SpiringBoot

    微服务: 微服务是一种架构风格,一般说到微服务都会说"微服务架构",即一个系统的各个功能(如结账,用户等)独立出来,以及各个服务独立出来,每个模块是可独立替换.可独立升级的软件单元 ...

  9. 4、架构--NFS实践、搭建web服务、文件共享

    笔记 1.晨考 1.数据备份的方式有哪些 全量和增量 2.数据备份的命令有哪些,都有哪些优点缺点 cp : 本地,全量复制 scp :远程,全量复制 rsync :远程,增量复制 3.rsync的参数 ...

  10. Solution -「CF 1361E」James and the Chase

    \(\mathcal{Description}\)   Link.   给定 \(n\) 个点 \(m\) 条边的有向弱连通图.称一个点是"好点"当且仅当从该点出发,不存在到同一点 ...