有意思,發現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 module
和base module
中的fromJson2List
方法,我們來猜一猜上面程式碼執行的預期結果
第一條語句,有了上面的案例,顯然會返回List<LinkedTreeMap>
物件;那第二條呢?按道理也應該返回List<LinkedTreeMap>
物件,然而,事與願違,執行下看看,如下:
可以看到,app module
中fromJson2List
方法反序列化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 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官網來解答了,有知道原因的小夥伴,可以在評論區留言。
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技術。
對文章有何見解,或者有何技術問題,歡迎在評論區一起留言討論!