移動開發必知:Kotlin裏面一個神奇的BUG(勸官方早點修復)

語言: CN / TW / HK

人無完人,金無足赤,哪怕是官方,也免不了出現BUG。上次就和大家分享給過一篇關於Kotlin可能帶來的一個深坑(最後一次提醒)

今天要和大家分享的是Kotlin裏面一個神奇的BUG 原文地址:https://blog.csdn.net/m0_46962786/article/details/114027622

前言

本文將會通過具體的業務場景,由淺入深的引出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<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神奇在哪裏?繼續往下看

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<LinkedTreeMap>對象;那第二條呢?按道理也應該返回List<LinkedTreeMap>對象,然而,事與願違,執行下看看,如下:

可以看到,app module中fromJson2List 方法反序列化List<User>失敗了,而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開發必須掌握的知識點,希望對大家有幫助)

「其他文章」