关于android:有意思发现Kotlin一个神奇的bug

4次阅读

共计 6512 个字符,预计需要花费 17 分钟才能阅读完成。

1、前言

本文将会通过具体的业务场景,由浅入深的引出 Kotlin 的一个 bug,并告知大家这个 bug 的神奇之处,接着会率领大家去查找 bug 呈现的起因,最初去躲避这个 bug。

2、bug 复现

事实开发中,咱们常常会有将 Json 字符串 反序列化为一个对象问题,这里,咱们用 Gson 来写一段反序列代码,如下:

fun <T> fromJson(json: String, clazz: Class<T>): T? {
    return try {Gson().fromJson(json, clazz)                  
    } catch (ignore: Exception) {null}                                                       
}     

以上代码,仅实用于不带泛型的类,对于带泛型的类,如List<T>,咱们就要再革新一下,如下:

fun <T> 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<User> 对象,其它带有泛型的类,皆可用此办法序列化
val type = object : TypeToken<List<User>>() {}.type
val users: List<User>? = fromJson("[{..},{...}]", type)

以上写法,是 Java 的语法翻译过去的,它有一个毛病,那就是泛型的传递必须要通过另一个类去实现,下面咱们借助类 TypeToken 类,置信这一点,很多人都不能承受,于是乎,在 Kotlin 上,呈现了一个新的关键字 reified(这里不开展介绍,不理解的自行查阅相干材料),它联合 kotlin 的内联(inline) 函数的个性,便能够间接在办法外部获取具体的泛型类型,咱们再次把下面的办法革新下,如下:

inline fun <reified T> 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<User>("{...}}")
val users = fromJson<List<User>>("[{..},{...}]")

当咱们满怀信心的测试以上代码时,问题呈现了,List<User>反序列化失败了,如下:

List 外面的对象竟不是 User,而是 LinkedTreeMap,怎么回事,这难道就是题目所说的 Kotlin 的 bug?当然不是!

咱们回到 fromJson 办法中,看到外部传递的是 T::class.java 对象,即 class 对象,而 class 对象有泛型的话,在运行期间泛型会被擦除,故如果是 List<User> 对象,运行期间就变成了 List.class 对象,而 Gson 在收到的泛型不明确时,便会主动将 json 对象 反序列化为 LinkedTreeMap 对象。

怎么解决?好办,咱们借助 TypeToken 类传递泛型即可,而这次,咱们仅须要在办法外部写一次即可,如下:

inline fun <reified T> fromJson(json: String): T? {
    return try {
        // 借助 TypeToken 类获取具体的泛型类型
        val type = object : TypeToken<T>() {}.type
        return Gson().fromJson(json, type)
    } catch (e: Exception) {null}
}

此时,咱们再来测试下上述的代码,如下:

能够看到,这次不论是 User,还是List<User> 对象,都反序列化胜利了。

到此,有人会有疑难,叨叨了这么多,说好的 Kotlin 的 bug 呢?别着急,持续往下看,bug 就快要呈现了。

忽然有一天,你的 leader 过去跟你说,这个 fromJson 办法还能不能再优化一下,当初每次反序列化 List 汇合,都须要在 fromJson 后写上<List<>>,这种场景十分多,写起来稍微有点繁琐。

此时你心里一万个那啥蹦腾而过,不过静下来想想,leader 说的也并不是没有情理,如果遇到多层泛型的状况,写起来就会更加繁琐,如:fromJson<BaseResponse<List<User>>>,

于是就开启了优化之路,把罕用的泛型类进行解耦,最初,你写出了如下代码:

inline fun <reified T> fromJson2List(json: String) = fromJson<List<T>>(json)

测试下,咦?惊呆了,似曾相识的问题,如下:

这又是为什么?fromJson2List外部仅调用了 fromJson 办法,为啥 fromJson 能够,fromJson2List却失败了,百思不得其解。

难道这就是题目说的 Kotlin 的 bug?很负责任的通知你,是的;

bug 神奇在哪里?持续往下看

3、bug 的神奇之处

咱们从新梳理下整个事件,下面咱们先定义了两个办法,把它们放到 Json.kt 文件中,残缺代码如下:

@file:JvmName("Json")

package com.example.test

import com.google.gson.Gson
import com.google.gson.reflect.TypeToken

inline fun <reified T> fromJson2List(json: String) = fromJson<List<T>>(json)

inline fun <reified T> fromJson(json: String): T? {
    return try {val type = object : TypeToken<T>() {}.type
        return Gson().fromJson(json, type)
    } catch (e: Exception) {null}
}

接着新建 User 类,残缺代码如下:

package com.example.bean

class User {val name: String? = null}

随后又新建一个 JsonTest.kt 文件,实现代码如下:

@file:JvmName("JsonTest")

package com.example.test

fun main() {val user = fromJson<User>("""{"name":" 张三 "}""")
    val users = fromJson<List<User>>("""[{"name":" 张三 "},{"name":" 李四 "}]""")
    val userList = fromJson2List<User>("""[{"name":" 张三 "},{"name":" 李四 "}]""")
    print("")
}

留神:这 3 个类在同一个包名下,且在同一个 Module 中

最初执行 main 办法,就会发现所说的 bug。

留神,后方高能:咱们把 Json.kt 文件拷贝一份到 Base Module 中,如下:

@file:JvmName("Json")

package com.example.base

import com.google.gson.Gson
import com.google.gson.reflect.TypeToken

inline fun <reified T> fromJson2List(json: String) = fromJson<List<T>>(json)

inline fun <reified T> fromJson(json: String): T? {
    return try {val type = object : TypeToken<T>() {}.type
        return Gson().fromJson(json, type)
    } catch (e: Exception) {null}
}

随后咱们在 app module 里的 Json.kt 文件中退出一个测试方法,如下:

fun test() {val users = fromJson2List<User>("""[{"name":" 张三 "},{"name":" 李四 "}]""")
    val userList = com.example.base.fromJson2List<User>("""[{"name":" 张三 "},{"name":" 李四 "}]""")
    print("")
}

注:在 base module 里的 Json.kt 文件中没有这个办法

下面代码中,别离执行了 app modulebase module中的 fromJson2List 办法,咱们来猜一猜下面代码执行的预期后果

第一条语句,有了下面的案例,显然会返回 List<LinkedTreeMap> 对象;那第二条呢?按情理也应该返回 List<LinkedTreeMap> 对象,然而,大失所望,执行下看看,如下:

能够看到,app modulefromJson2List 办法反序列化List<User> 失败了,而 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<T> {}

  public static final class Json$fromJson2List$$inlined$fromJson$1 extends TypeToken<List<? extends T>> {}}

能够看到,Json.kt外面的两个内联办法,编译为字节码文件后,变成了两个动态外部类,且都继承了 TypeToken 类,看起来没啥问题,

持续看看 app moduleJson.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<T> {}

  public static final class Json$fromJson2List$$inlined$fromJson$1 extends TypeToken<List<? extends T>> {}

  public static final class Json$fromJson2List$$inlined$fromJson$2 extends TypeToken<List<? extends T>> {}

  public static final class Json$test$$inlined$fromJson2List$1 extends TypeToken<List<? extends User>> {}}

在该字节码文件中,有 1 个 test 办法 + 4 个动态外部类;前两个动态外部类,就是Json.kt 文件中两个内联办法编译后的后果,这个能够不必管。

接着,来看看 test 办法,该办法有两次反序列化过程,第一次调用了动态外部类 JsonfromJson2List$$inlinedfromJson$2`,第二次调用了动态外部类 `Jsontest$$inlinedfromJson2List$1,也就是别离调用了第三、第四个动态外部类去获取具体的泛型类型,而这两个动态外部类申明的泛型类型是不一样的,别离是<List<? extends T>><List<? extends User>>,到这,预计大伙都明确了,显然第一次反序列化过程泛型被擦除了,所以导致了反序列化失败。

至于为什么依赖本 module 的办法,遇到泛型 T 与具体类相结合时,泛型 T 会被擦除问题,这个就须要 Kotlin 官网来解答了,有晓得起因的小伙伴,能够在评论区留言。

5、扩大

如果你的我的项目没有依赖Gson,能够自定义一个类,来获取具体的泛型类型,如下:

open class TypeLiteral<T> {
    val type: Type
        get() = (javaClass.genericSuperclass as ParameterizedType).actualTypeArguments[0]
}

// 用以下代码替换 TypeToken 类相干代码即可
val type = object : TypeLiteral<T>() {}.type

对于泛型的组合,还能够用 RxHttp 库外面的 ParameterizedTypeImpl 类,用法如下:

// 失去 List<User> 类型
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

文末

您的点赞珍藏就是对我最大的激励!
欢送关注我,分享 Android 干货,交换 Android 技术。
对文章有何见解,或者有何技术问题,欢送在评论区一起留言探讨!

正文完
 0