持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第6天,点击查看活动详情
前言
前段时间有涉及过一个Gson解析的json带泛型的情况,然后我的旧博客正好写过一篇泛型解析的文章 www.jianshu.com/p/4f797b1f8… ,所以我直接拿上面的代码来用就能正常解析,很快就解决了问题,但是毕竟是多年前的文章,想在它的基础上再写一些知识的扩展。 为什么要说泛型擦除,因为如果你想要知道为什么能gson能解析泛型,你就要先了解泛型擦除是什么。
泛型擦除
什么是泛型擦除
Java 泛型的类型参数在编译时会被消除,所以无法在运行时得知其类型参数的类型
简单来说就是你代码写的会在编译后变为Object
举例泛型擦除
我们来看一段Demo,我写的带泛型的类
public class Test<T> {
private T data;
public void setData(T t){
data = t;
Log.v("mmp", "查看设置结果: "+data);
}
public T getData(){
return data;
}
}
我在外部这点调用会提示报错,这就是泛型的作用,起到了约束。 那有没有办法,添加String类型的数据进去呢?当然有,这里就体现出泛型擦除。我如果用反射来添加数据,能不能添加成功?
val test = Test<Int>()
val cls = Test::class.java
try {
val method = cls.getDeclaredMethod("setData", Any::class.java)
method.invoke(test, "aaa")
}catch (e : Exception){
e.printStackTrace()
}
我如果这样运行代码,是能看到上面的打印结果“查看设置结果:aaa”是能正常打印的,这就表现出了泛型擦除。
那么这里再抛出一个问题,我反射的getDeclaredMethod方法,如果传的不是Any::class.java (这个是kotlin的写法,相当于java中的Object.class),我如果传的是String.class或者Int.class能否正常执行?
如果回答不出这个问题,说明你还是不明白泛型擦除。
泛型擦除后怎么知道是什么类型
很多人可能会有这样的问题,例如代码中的val test = Test(),我这里如果调用setData(1)进去,再拿getData(),那这个地方怎么知道我拿的是int类型,而不是反的Object,你不是说擦除了吗,那怎么想都是反的Object啊?
小了,格局小了。所以说泛型擦除,不是那么简单的知识,不是说就只是把在运行时变成Object而已,当然没这么简单,这其中的知识点涉及JVM,涉及Class这些。
我这里直接说结论:因为会做类型转换,所以转换回了int类型。
而且这个类型转换是在外层做的,不是在内层做的。什么意思呢?就是看上面我那个方法setData(),方法里面有一句打印,我说了,用反射把String类型添加进去,能正常打印。你想想,如果这里是在内部做强转,我这里能正常打印吗,当然会直接报类型错误。
那再来看看如果我把外层的代码写成这样会发生什么?
val test = Test<Int>()
val cls = Test::class.java
try {
val method = cls.getDeclaredMethod("setData", Any::class.java)
method.invoke(test, "aaa")
}catch (e : Exception){
e.printStackTrace()
}
val result = test.data
Log.v("mmp", "测试反射泛型的结果: $result")
能正常打印出结果吗?当然不能,因为在外层强转出现了错误。
那怎么知道他有做强转?我这里直接给结论,其实它在转成字节码之后,会有一个checkcast指令,就是这条代码做的强转。这里如果往深的讲,这篇就讲不完了,所以只讲大概的原理。
泛型擦除后会保留泛型信息吗?
这个问题就是过度到Gson解析泛型,你觉得泛型擦除后会保存泛型信息吗?当然会,不然我Gson怎么解析出来,我不知道他什么类型的我怎么解析出来。
那它是怎么保存信息的,不是擦除成Object了吗,难道Object还能存泛型信息,难道我们能直接在代码中用T.class拿到泛型的类?当然不是。
还是直接讲结论,这涉及到一个东西叫class signature,泛型信息就被保存在这里面,保存在常量池中,所以Gson能够从中拿到泛型的信息,从而解析出来。具体的也是涉及比较深,这里就不展开讲。
小结
先讲泛型擦除,主要是为了抛出两个问题,(1)擦除后外层怎么知道是什么类型。(2)擦除后会不会保存泛型的信息。根据这两个问题的答案,就能很清楚的知道Gson为什么能在泛型擦除的情况下还能解析泛型。如果你还是想去深究这两个问题,我可以暂时提供一篇文章,这哥们写得挺好的,blog.csdn.net/qq_32452623… 但也只是说了个大概,如果你想知道所有的细节,还要去往更深的看。
Gson解析泛型
PS:这是我很久之前写的旧文章,其中写的Demo是另一个Demo,不要和上面的Demo搞混。
使用Gson来解析JSON
我们先看看平时如何使用Gson来解析json,就假设有个Test类吧。
Test test = gson.fromJson(json, Test.class);
一般在明确对象类型之后我们确实可以这样做,但是如果是泛型呢
T test = gson.fromJson(json, T.class);
肯定不能这样玩,这不符合泛型的思想,而且也没有T.class,所以需要换种方法来做
这个方法的第二个参数是传一个Type,我们可以来看看什么是Type
获取Type
一般你可以很容易的找到这样做
Type type = new TypeToken<Test>() {}.getType();
JsonBean jsonBean = gson.fromJson(json, type);
然后你觉得如果解析泛型的话可以这样
Type type = new TypeToken<T>() {}.getType();
JsonBean jsonBean = gson.fromJson(json, type);
这个我没试过,但是我知道如果传的不是泛型,而是一个包含泛型的类,最后解析出来的还是LinkedTreeMap,比如这样
Type type = new TypeToken<Result<T>>() {}.getType();
JsonBean jsonBean = gson.fromJson(json, type);
因为一般网络请求的数据结构我们都会这样做Result,而这样是没法正常解析出我们想要的对象的。
这个时候网上就有一篇文章写得特别好
www.jianshu.com/p/d62c2be60…
可以看到需要重写一个类继承ParameterizedType
public class ParameterizedTypeImpl implements ParameterizedType {
private final Class raw;
private final Type[] args;
public ParameterizedTypeImpl(Class raw, Type[] args) {
this.raw = raw;
this.args = args != null ? args : new Type[0];
}
@Override
public Type[] getActualTypeArguments() {
return args;
}
@Override
public Type getRawType() {
return raw;
}
@Override
public Type getOwnerType() {return null;}
}
然后按照他的写法
Type type = new ParameterizedTypeImpl(Result.class, new Class[]{clazz});
那么假如我写一个解析类,我要怎么去获取到type,我就假如我自己写的okhttp的回调
public abstract class HttpCallback<T> implements Callback {
........
}
我要怎么在内部根据泛型来获取到Type对象,然后再根据type解析出javabean,还是假如我们的泛型从外面传进来的是Test
其实网上有很多代码,我可以一步一步调试分析出对象是什么
Type type = getClass().getGenericSuperclass();
这个方法也可以获取到Type对象,getClass()是Object类的方法,得到当前类的Class对象,这个很容易懂吧,getGenericSuperclass()是Class类的方法,得到这个Class类对象的Type对象,我们可以调试发现这里的getClass().getGenericSuperclass()为HttpCallback
拿到这个对象之后发现发现别人都是这样写的
Type[] types = ((ParameterizedType)type).getActualTypeArguments();
这里用到了一个ParameterizedType去强转对象,ParameterizedType中其实一共有是三个方法
(1)getActualTypeArguments()
(2)getOwnerType()
(3)getRawType()
我们来分别看看他们得到的结果
可以看到getActualTypeArguments得到的一个数组当前只有一个元素types[0] = Test。
getOwnerType()得到null。
getRawType()得到HttpCallback
从这里我们就可以知道,我们需要使用的是getActualTypeArguments()方法来获取我们要的Type
现在再来想想,我们需要的Type是用泛型转成的Result
getActualTypeArguments获得Test,getRawType()获得外层的类型,那把他们拼起来
public class ParameterizedTypeImpl implements ParameterizedType {
private final Class raw;
private final Type[] args;
public ParameterizedTypeImpl(Class raw, Type[] args) {
this.raw = raw;
this.args = args != null ? args : new Type[0];
}
@Override
public Type[] getActualTypeArguments() {
return args;
}
@Override
public Type getRawType() {
return raw;
}
@Override
public Type getOwnerType() {return null;}
}
就能获取到raw<args[0]>的type,那我们要获取Result,raw就是Result.class,可以根据((ParameterizedType)type).getActualTypeArguments()来获取。那我们可以这样写
Class<T> tClass = (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0]; // 根据当前类获取泛型的Type
Type ty = new ParameterizedTypeImpl(BaseResponse.class, new Class[]{tClass}); // 传泛型的Type和我们想要的外层类的Type来组装我们想要的类型
JsonBean jsonBean = gson.fromJson(json, ty);
这样就能正常的解析到我们想要的javabean,注意,这个过程中最主要的是一个组装的思想,用到ParameterizedType 。那有的人问,为什么不可以直接用ParameterizedType 来操作,我们可以看看ParameterizedType 。
可以看到它是一个接口,继承Type
为什么要说这个,因为我感觉这个和他Type的一个思想有些关系,要理解别人是怎么去定义这个东西是什么,我们应该怎么去把这个东西说出来。先看getActualTypeArguments的注释
Returns an array of {@code Type} objects representing the actual type arguments to this type.
这里有个actual表示真实的,我们获取到的type[]数组内部的元素就是actual,那有必要看看什么是actual,现在我只定义了一个泛型,type[]数组也就只有一个actual,这个actual会得到传进来的泛型的类型,那假如我没有定义泛型和定义两个泛型呢?
(1)没有定义泛型的情况
如果没有泛型的话,getClass().getGenericSuperclass()会得到HttpCallBack,((ParameterizedType)type).getActualTypeArguments()会报类型转换错误: java.lang.ClassCastException: java.lang.Class cannot be cast to java.lang.reflect.ParameterizedType
(2)两个泛型的情况
getClass().getGenericSuperclass()会得到HttpCallBack<String,String>,我外面传的是两个String。
((ParameterizedType)type).getActualTypeArguments()得到的数组有两个元素
(3)当类不是抽象类的时候
上面的方法都是我在定义的类是抽象类的时候才能起作用,比如我两个参数的类
那么不是抽象类的时候,getClass().getGenericSuperclass()不管你传多少个参数都会得到Object
((ParameterizedType)type).getActualTypeArguments()自然也会报参数错误
那么如果是List数组的话呢?我们传入的泛型如果是List的话
Type type = getClass().getGenericSuperclass();
得到HttpCallback<List>
Type[] types = ((ParameterizedType)type).getActualTypeArguments();
types[0]得到的是List
这样就能把types[0]和我们想要的对象Result传到自定义的ParameterizedType中组装。
可以看出我们就不需要像之前一样获取Class对象,因为这个对象没法装List,直接获取到Type[] types就行。
总结
1.gson解析泛型的方法
Type type = getClass().getGenericSuperclass();
Type[] types = ((ParameterizedType)type).getActualTypeArguments();
Type ty = new ParameterizedTypeImpl(Result.class, new Type[]{types[0]});
Result<T> data = gson.fromJson(josndata, ty);
2.解析时的限制
(1)type不能转成ParameterizedType的情况
上面说了没定义泛型的时候type转成ParameterizedType会报类型转换错误,所以要加个判断
Type type = getClass().getGenericSuperclass();
if(type instanceof ParameterizedType){
Type[] types = ((ParameterizedType)type).getActualTypeArguments();
Type ty = new ParameterizedTypeImpl(Result.class, new Type[]{types[0]});
Result<T> data = gson.fromJson(josndata, ty);
}
(2)当前类不为抽象类的话type会得到Object,虽然我们用上面的判断能防止报错,但是这样也无法正常解析出数据,所以我们这个解析泛型的类必须是抽象类。至于为什么只有抽象类才能正常解析出而一般的类的type都拿到是Object,这点我就不是很清楚了。
3.Type[]数组
Type[]数组types中的数量是泛型的数量,比如传的泛型是<String,Test>,那这个数组就有两个值
types[0] = String
types[1] = Test
4.传的泛型是一个数组
public abstract class HttpCallBack<T> {
public HttpCallBack(){
init();
}
private void init(){
Type type = getClass().getGenericSuperclass();
Type[] types = ((ParameterizedType)type).getActualTypeArguments();
}
}
new HttpCallBack<List<String>>(){
};
直接贴结果
5.自定义一个ParameterizedType来拼接类型
public class ParameterizedTypeImpl implements ParameterizedType {
private final Class raw;
private final Type[] args;
public ParameterizedTypeImpl(Class raw, Type[] args) {
this.raw = raw;
this.args = args != null ? args : new Type[0];
}
@Override
public Type[] getActualTypeArguments() {
return args;
}
@Override
public Type getRawType() {
return raw;
}
@Override
public Type getOwnerType() {return null;}
}
Type ty = new ParameterizedTypeImpl(Result.class, new Type[]{types[0]});
拿到type之后调gson就行了