发现Kotlin一个神奇的bug

不怕天黑 等级 444 2 0

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

发现Kotlin一个神奇的bug

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
    }
}

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

发现Kotlin一个神奇的bug

可以看到,这次不管是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)

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

发现Kotlin一个神奇的bug

这又是为什么?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>对象,然而,事与愿违,执行下看看,如下:

发现Kotlin一个神奇的bug

可以看到,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,真的很不错的一个库,强烈推荐

收藏
评论区

相关推荐

WorkManager 在 Kotlin 中的实践
WorkManager 是一个 Android Jetpack 扩展库,它可以让您轻松规划那些可延后、异步但又需要可靠运行的任务。对于绝大部分后台执行任务来说,使用 WorkManager 是目前 Android 平台上的最佳实践。 WorkManager https://developer.android.google.cn/topic/libra
Flutter开发必备Dart基础:Dart快速入门
<h1概述</h1 <pDart从2.0开始变为强类型语言,静态类型。这点和Java、C等比较相似。也就是说在编译时就已经知道变量的类型那么就是静态类型语言。开发人员在开发的时候需要指定变量的类型。这有什么优点呢? 就是所有类型检查都可以通过编译器来完成。可以提前预报一些琐碎的错误。<br 同时Dart还是面向对象的编程语言。像python、Java、Kol
Golang泛型编程初体验
序言 众所周知,Golang中不支持类似C/Java中的标记式泛型,所以对于常用算法,比如冒泡排序算法,有些同学容易写出逻辑上重复的代码,即整型是第一套代码,字符串型是第二套代码,用户自定义类型是第三套代码。 重复是万恶之源,我们当然不能容忍,所以要消除重复,使得代码保持在最佳的状态。本文通过一个实际使用的简单算法的演进过程,初次体验了Golan
发现Kotlin一个神奇的bug
1、前言 本文将会通过具体的业务场景,由浅入深的引出Kotlin的一个bug,并告知大家这个bug的神奇之处,接着会带领大家去查找bug出现的原因,最后去规避这个bug。 2、bug复现 现实开发中,我们经常会有将Json字符串反序列化为一个对象问题,这里,我们用Gson来写一段反序列代码,如下: kotlin fun <T fromJson(js
协变和逆变
本文同步发表于我的微信公众号,在微信搜索 OpenCV or Android 即可关注。 协变、逆变 概念 许多程序设计语言的类型系统支持子类型。例如,如果Cat是Animal的子类型,那么Cat类型的表达式可用于任何出现Animal类型表达式的地方。所谓的变型(variance)是指如何根据组成类型之间的子类型关系,来确定更复杂的类型之间(例如C
GitHub最牛的Android开源项目,金三银四
Android目前也是非常成熟的,如今移动的变化也是比较大,不管是从Android系统本身的变化还是语言上,自从17年kotlin的作为第一语言,涉及到Android系统周边的非常之多内容,若全部自己去实现及掌握可能需要花费很多的精力,现在github中有很多非常优秀的项目,快速的学习及掌握; 1,视频播放器,基于IJKPlayer(兼容系统MediaPl
Python中的基本list操作
List是python中的基本数据结构之一,和Java中的ArrayList有些类似,支持动态的元素的增加。list还支持不同类型的元素在一个列表中,List is an Object。 最基本的创建一个列表的方法 myList \'a','b','c'\ 在python中list也是对象,所以他也有方法和属性,在ptython解释器中 使用h
java 泛型详解-绝对是对泛型方法讲解最详细的,没有之一
java 泛型详解绝对是对泛型方法讲解最详细的,没有之一 对java的泛型特性的了解仅限于表面的浅浅一层,直到在学习设计模式时发现有不了解的用法,才想起详细的记录一下。 本文参考、、 1、概述泛型在java中有很重要的地位,在面向对象编程及各种设计模式中有非常广泛的应用。什么是泛型?
死磕Java泛型(一篇就够)
Java泛型,算是一个比较容易产生误解的知识点,因为Java的泛型基于擦除实现,在使用Java泛型时,往往会受到泛型实现机制的限制,如果不能深入全面的掌握泛型知识,就不能较好的驾驭使用泛型,同时在阅读开源项目时也会处处碰壁,这一篇就带大家全面深入的死磕Java泛型。 泛型擦除初探相信泛型大家都使用过,所以一些基础的知识点就不废话了,以免显得啰嗦。
Dart中的泛型、泛型方法、泛型类、泛型接口
一、Dart中的泛型 泛型方法 通俗理解:泛型就是解决 类 接口 方法的复用性、以及对不特定数据类型的支持(类型校验) 一般用   T   表示泛型 getData<T(T value){ return
Kotlin 协程中,关于 runBlocking, launch ,withContext ,async,doAsync 之间的简单区别
引入大佬的话,Kotlin的协程,本质上是一个线程框架,它可以方便的切换线程的上下文(如主线程切换到子线程/子线程切回主线程)。而平时我们要想在Android Studio使用协程,先要在gradle引入协程依赖: implementation "org.jetbrains.kotlinx:kotlinxcoroutinescore:1.3.3"
go语言坑之list删除所有元素
go提供了一个list包 类似python的list,可以存储任意类型的数据,并提供了相应的API,如下:type Element func (e Element) Next() Element func (e Element) Prev() Elementtype List func New() List func (l List)
Java泛型使用之实现一个能够对字符、整型、浮点型、字节型、对象进行大小比较的方法
定义一个泛型类,定义泛型T,存储咱们的最大值最小值,重载构造函数,存储最大最小值,重写toString方法; package person.xsc.practice;public class Num<T //定义最大最小值 public T max; public T min; public Num() //构造函数(有参) public Num(T
我丢,去面试初级Java开发岗位,被问到泛型?
1、泛型的基础概念 1.1 为什么需要泛型 c List list new ArrayList();//默认类型是Object list.add("A123"); list.add("B234"); list.add("C345"); System.out.println(list);
全靠这份Android知识点PDF大全,月薪30K
第一阶段:Android 基础知识回顾: 回顾Android 开发编程,深入理解Android系统原理和层次结构,深入分析Handler源码和原理; 回顾Java,C/C++,Kotlin、dart 在Android开发中必用的语言,熟悉一下几种语言混淆后的特性; 回顾Android IPC和JNI的底层原理和热更新技术回顾Native开发要点,使用C++结

热门文章

最新文章