1. 程式人生 > 其它 >有意思,發現Kotlin一個神奇的bug!

有意思,發現Kotlin一個神奇的bug!

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

於是就開啟了優化之路,把常用的泛型類進行解耦,最後,你寫出瞭如下程式碼:

inlinefun<reifiedT>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技術。
對文章有何見解,或者有何技術問題,歡迎在評論區一起留言討論!