• 移动开发必知:Kotlin里面一个神奇的BUG(劝官方早点修复)


    人无完人,金无足赤,哪怕是官方,也免不了出现BUG。上次就和大家分享给过一篇关于Kotlin可能带来的一个深坑(最后一次提醒)

    今天要和大家分享的是Kotlin里面一个神奇的BUG
    原文地址:https://juejin.cn/post/6921359126438084621

    前言

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

    bug复现

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

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

    以上代码,仅适用于不带泛型的类,对于带泛型的类,如List,我们就要再改造一下,如下:

    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反序列化失败了,如下:

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

    我们回到fromJson方法中,看到内部传递的是T::class.java对象,即class对象,而class对象有泛型的话,在运行期间泛型会被擦除,故如果是List对象,运行期间就变成了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对象,都反序列化成功了。

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

    突然有一天,你的leader过来跟你说,这个fromJson方法还能不能再优化一下,现在每次反序列化List集合,都需要在fromJson后写上<List<>>,这种场景非常多,写起来略微有点繁琐。

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

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

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

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

    这又是为什么?fromJson2List内部仅调用了fromJson方法,为啥fromJson可以,fromJson2List却失败了,百思不得其解。

    难道这就是标题说的Kotlin的bug?很负责任的告诉你,是的。

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

    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 module和base module中的fromJson2List方法,我们来猜一猜上面代码执行的预期结果。

    第一条语句,有了上面的案例,显然会返回List对象;那第二条呢?按道理也应该返回List对象,然而,事与愿违,执行下看看,如下:

    可以看到,app module中fromJson2List 方法反序列化List失败了,而base module中的fromJson2List 方法却成功了。

    同样的代码,只是所在module不一样,执行结果也不一样,你说神不神奇?

    一探究竟

    知道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 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<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官网来解答了,有知道原因的小伙伴,可以在评论区留言。

    扩展

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

    open class TypeLiteral<T> {
        val type: Type
            get() = (javaClass.genericSuperclass as ParameterizedType).actualTypeArguments[0]
    }
    
    //用以下代码替换TypeToken类相关代码即可
    val type = object : TypeLiteral<T>() {}.type
    

    对于泛型的组合,还可以用RxHttp库里面的ParameterizedTypeImpl类,用法如下:

    https://github.com/liujingxing/okhttp-RxHttp

    https://github.com/liujingxing/okhttp-RxHttp/blob/master/rxhttp/src/main/java/rxhttp/wrapper/entity/ParameterizedTypeImpl.kt

    //得到 List<User> 类型
    val type: Type = ParameterizedTypeImpl[List::class.java, User::class.java]
    

    详细用法可查看Android、Java泛型扫盲。

    https://juejin.cn/post/6844903828219756552

    小结

    目前要规避这个问题的话,将相关代码移动到子module即可,调用子module代码就不会有泛型擦除问题。

    这个问题,其实在kotlin 1.3.x版本时,我就发现了,到目前最新版本也一直存在,期间曾请教过Bennyhuo大神,后面规避了这个问题,就没放心上,近期将会把这个问题,提交给kotlin官方,望尽快修复。

    笔者感想

    自谷歌宣布了Kotlin-First 这一重要概念以来,官方就一直主张Kotlin 是 Android 开发者的首选语言。

    但是,直到现在还有很多人心中有一个疑问:那我一个搞Android开发的,我现在是不是以后就只用学Kotlin 就可以了?

    我的回答是:不可以。Java任然是现在的搞Android开发的主流语言。另外,C语言也很重要(音视频开发方向很热门)。

    很多人觉得搞Android开发会用轮子就可以了,但是轮子不是万能的,现在的Android人才市场爆满,要想脱颖而出,需要掌握的东西还有很多,大家如果想要有长久的发展,希望大家可以好好看一下下面列举的点。

    耗时298天,8大模块、3382页66万字,Android开发核心知识笔记!(这里面都是大佬根据自己多年的工作经验总结出来的,也是现在搞Android开发必须掌握的知识点,希望对大家有帮助)

  • 相关阅读:
    B 合约数
    Weekly Contest 80
    第十六届上海大学程序设计联赛春季赛暨上海高校金马五校赛
    2018年长沙理工大学第十三届程序设计竞赛
    通过 DDNS 解决宽带拨号 ip 变化问题
    frp 内网穿透的使用 和原理
    笔记本centos 取消 关闭盖子自动挂起
    centos7 pptp 安装
    linux 命令收集
    centos7 关闭防护墙
  • 原文地址:https://www.cnblogs.com/chengsisi/p/14442349.html
Copyright © 2020-2023  润新知