文章图片
1、前言
本文将会通过具体的业务场景,由浅入深的引出Kotlin的一个bug,并告知大家这个bug的神奇之处,接着会带领大家去查找bug出现的原因,最后去规避这个bug。
2、bug复现
现实开发中,我们经常会有将Json字符串
反序列化为一个对象问题,这里,我们用Gson
来写一段反序列代码,如下:
fun fromJson(json: String, clazz: Class): T? {
return try {
Gson().fromJson(json, clazz)
} catch (ignore: Exception) {
null
}
}
以上代码,仅适用于不带泛型的类,对于带泛型的类,如
List
,我们就要再改造一下,如下:fun fromJson(json: String, type: Type): T? {
return try {
return Gson().fromJson(json, type)
} catch (e: Exception) {
null
}
}
此时,我们就可以借助于
Gson
里面的TypeToken
类,从而实现任意类型的反序列化,如下://1、反序列化User对象
val user: User? = fromJson("{...}}", User::class.java)//2、反序列化List对象,其它带有泛型的类,皆可用此方法序列化
val type = object : TypeToken>() {}.type
val users: List? = fromJson("[{..},{...}]", type)
以上写法,是Java的语法翻译过来的,它有一个缺点,那就是泛型的传递必须要通过另一个类去实现,上面我们借助类
TypeToken
类,相信这一点,很多人都不能接受,于是乎,在Kotlin
上,出现了一个新的关键字reified
(这里不展开介绍,不了解的自行查阅相关资料),它结合kotlin的内联(inline)函数的特性,便可以直接在方法内部获取具体的泛型类型,我们再次把上面的方法改造下,如下:inline fun fromJson(json: String): T? {
return try {
return Gson().fromJson(json, T::class.java)
} catch (e: Exception) {
null
}
}
可以看到,我们在方法前加上了
inline
关键字,表明这是一个内联函数;接着在泛型T
前面加上reified
关键字,并把方法里不需要的Type
参数去掉;最后我们通过T::class.java
传递具体的泛型类型,具体使用如下:val user = fromJson("{...}}")
val users = fromJson>("[{..},{...}]")
当我们满怀信心的测试以上代码时,问题出现了,
List
反序列化失败了,如下:文章图片
List里面的对象竟不是User,而是LinkedTreeMap,怎么回事,这难道就是标题所说的Kotlin的bug?当然不是!
我们回到
fromJson
方法中,看到内部传递的是T::class.java
对象,即class对象,而class对象有泛型的话,在运行期间泛型会被擦除,故如果是List
对象,运行期间就变成了List.class
对象,而Gson
在收到的泛型不明确时,便会自动将json对象
反序列化为LinkedTreeMap
对象。怎么解决?好办,我们借助
TypeToken
类传递泛型即可,而这次,我们仅需要在方法内部写一次即可,如下:inline fun fromJson(json: String): T? {
return try {
//借助TypeToken类获取具体的泛型类型
val type = object : TypeToken() {}.type
return Gson().fromJson(json, type)
} catch (e: Exception) {
null
}
}
此时,我们再来测试下上述的代码,如下:
文章图片
可以看到,这次不管是
User
,还是List
对象,都反序列化成功了。到此,有人会有疑问,叨叨了这么多,说好的Kotlin的bug呢?别着急,继续往下看,bug就快要出现了。
突然有一天,你的leader过来跟你说,这个
fromJson
方法还能不能再优化一下,现在每次反序列化List
集合,都需要在fromJson
后写上>
,这种场景非常多,写起来略微有点繁琐。此时你心里一万个那啥蹦腾而过,不过静下来想想,leader说的也并不是没有道理,如果遇到多层泛型的情况,写起来就会更加繁琐,如:
fromJson>>
,于是就开启了优化之路,把常用的泛型类进行解耦,最后,你写出了如下代码:
inline fun fromJson2List(json: String) = fromJson>(json)
测试下,咦?惊呆了,似曾相识的问题,如下:
文章图片
这又是为什么?
fromJson2List
内部仅调用了fromJson
方法,为啥fromJson
可以,fromJson2List
却失败了,百思不得其解。难道这就是标题说的Kotlin的bug?很负责任的告诉你,是的;
bug神奇在哪里?继续往下看
3、bug的神奇之处 我们重新梳理下整个事件,上面我们先定义了两个方法,把它们放到
Json.kt
文件中,完整代码如下:@file:JvmName("Json")package com.example.testimport com.google.gson.Gson
import com.google.gson.reflect.TypeTokeninline fun fromJson2List(json: String) = fromJson>(json)inline fun fromJson(json: String): T? {
return try {
val type = object : TypeToken() {}.type
return Gson().fromJson(json, type)
} catch (e: Exception) {
null
}
}
接着新建
User
类,完整代码如下:package com.example.beanclass User {
val name: String? = null
}
随后又新建一个
JsonTest.kt
文件,完成代码如下:@file:JvmName("JsonTest")package com.example.testfun main() {
val user = fromJson("""{"name": "张三"}""")
val users = fromJson>("""[{"name": "张三"},{"name": "李四"}]""")
val userList = fromJson2List("""[{"name": "张三"},{"name": "李四"}]""")
print("")
}
注意:这3个类在同一个包名下,且在同一个Module中
最后执行main方法,就会发现所说的bug。
注意,前方高能:我们把
Json.kt
文件拷贝一份到Base Module
中,如下:@file:JvmName("Json")package com.example.baseimport com.google.gson.Gson
import com.google.gson.reflect.TypeTokeninline fun fromJson2List(json: String) = fromJson>(json)inline fun fromJson(json: String): T? {
return try {
val type = object : TypeToken() {}.type
return Gson().fromJson(json, type)
} catch (e: Exception) {
null
}
}
随后我们在
app module
里的Json.kt
文件中加入一个测试方法,如下:fun test() {
val users = fromJson2List("""[{"name": "张三"},{"name": "李四"}]""")
val userList = com.example.base.fromJson2List("""[{"name": "张三"},{"name": "李四"}]""")
print("")
}
注:在base module里的Json.kt文件中没有这个方法
上面代码中,分别执行了
app module
和base module
中的fromJson2List
方法,我们来猜一猜上面代码执行的预期结果第一条语句,有了上面的案例,显然会返回
List
对象;那第二条呢?按道理也应该返回List
对象,然而,事与愿违,执行下看看,如下:文章图片
可以看到,
app module
中fromJson2List
方法反序列化List
失败了,而base module
中的fromJson2List
方法却成功了。同样的代码,只是所在module不一样,执行结果也不一样,你说神不神奇?
4、一探究竟
知道bug了,也知道了bug的神奇之处,接下来就去探索下,为什么会这样?从哪入手?
显然,要去看
Json.kt
类的字节码文件,我们先来看看base module
里的Json.class
文件,如下:注:以下字节码文件,为方便查看,会删除一些注解信息
package com.example.base;
import com.google.gson.reflect.TypeToken;
import java.util.List;
public final class Json {public static final class Json$fromJson$type$1 extends TypeToken {}public static final class Json$fromJson2List$$inlined$fromJson$1 extends TypeToken> {}
}
可以看到,
Json.kt
里面的两个内联方法,编译为字节码文件后,变成了两个静态内部类,且都继承了TypeToken
类,看起来没啥问题,继续看看
app module
的Json.kt
文件对应的字节码文件,如下:package com.example.test;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.util.List;
public final class Json {
public static final void test() {
List list;
Object object = null;
try {
Type type = (new Json$fromJson2List$$inlined$fromJson$2()).getType();
list = (List)(new Gson()).fromJson("[{\"name\": \"\"},{\"name\": \"\"}]", type);
} catch (Exception exception) {
list = null;
}
(List)list;
try {
Type type = (new Json$test$$inlined$fromJson2List$1()).getType();
object = (new Gson()).fromJson("[{\"name\": \"\"},{\"name\": \"\"}]", type);
} catch (Exception exception) {}
(List)object;
System.out.print("");
}public static final class Json$fromJson$type$1 extends TypeToken {}public static final class Json$fromJson2List$$inlined$fromJson$1 extends TypeToken> {}public static final class Json$fromJson2List$$inlined$fromJson$2 extends TypeToken> {}public static final class Json$test$$inlined$fromJson2List$1 extends TypeToken> {}
}
在该字节码文件中,有
1个test方法 + 4个静态内部类
;前两个静态内部类,就是Json.kt
文件中两个内联方法编译后的结果,这个可以不用管。接着,来看看test方法,该方法有两次反序列化过程,第一次调用了静态内部类
JsonfromJson2List$$inlinedfromJson$2`,第二次调用了静态内部类`Jsontest$$inlinedfromJson2List$1
,也就是分别调用了第三、第四个静态内部类去获取具体的泛型类型,而这两个静态内部类声明的泛型类型是不一样的,分别是>
和>
,到这,估计大伙都明白了,显然第一次反序列化过程泛型被擦除了,所以导致了反序列化失败。至于为什么依赖本module的方法,遇到泛型
T
与具体类相结合时,泛型T
会被擦除问题,这个就需要Kotlin官网来解答了,有知道原因的小伙伴,可以在评论区留言。5、扩展
如果你的项目没有依赖
Gson
,可以自定义一个类,来获取具体的泛型类型,如下:open class TypeLiteral {
val type: Type
get() = (javaClass.genericSuperclass as ParameterizedType).actualTypeArguments[0]
}//用以下代码替换TypeToken类相关代码即可
val type = object : TypeLiteral() {}.type
对于泛型的组合,还可以用RxHttp库里面的ParameterizedTypeImpl类,用法如下:
//得到 List 类型
val type: Type = ParameterizedTypeImpl[List::class.java, User::class.java]
详细用法可查看Android、Java泛型扫盲
6、小结
目前要规避这个问题的话,将相关代码移动到子module即可,调用子module代码就不会有泛型擦除问题;
这个问题,其实在kotlin 1.3.x版本时,我就发现了,到目前最新版本也一直存在,期间曾请教过Bennyhuo大神,后面规避了这个问题,就没放心上,近期将会把这个问题,提交给kotlin官方,望尽快修复。
最后,给大家推荐一个网络请求库RxHttp,支持Kotlin协程、RxJava2、RxJava3,任意请求三步搞定,截止目前已有2.7k+ star,真的很不错的一个库,强烈推荐
Android高级开发系统进阶笔记、最新面试复习笔记PDF,我的GitHub
文章图片
文末 【有意思,发现Kotlin一个神奇的bug!】您的点赞收藏就是对我最大的鼓励!
欢迎关注我,分享Android干货,交流Android技术。
对文章有何见解,或者有何技术问题,欢迎在评论区一起留言讨论!
推荐阅读
- 【Android进阶】kotlin 委托
- 【Android进阶】kotlin委托属性
- 【Android进阶】Kotlin 条件控制
- Kotlin 接口
- 为什么要学Kotlin?Kotlin的未来趋势怎么样(会取代java吗?)
- Android Kotlin Coroutines ktx扩展
- Android Kotlin 协程async
- Android Kotlin Exception处理
- 为什么我用kotlin写了一个项目之后就开始后悔了