原創系列:java
翻譯系列:編程
實戰系列:安全
簡述: 今天咱們開始接着原創系列文章,首先說下爲何不把這篇做爲翻譯篇呢?我看了下做者的原文,裏面講到的,這篇博客都會有所涉及。這篇文章將會帶你所有弄懂Kotlin泛型中的reified實化類型參數,包括它的基本使用、源碼原理、以及使用場景。有了上篇文章的介紹,相信你們對kotlin的reified實化類型參數有了必定認識和了解。那麼這篇文章將會更加完整地梳理Kotlin的reified實化類型參數的原理和使用。廢話很少說,直接來看一波章節導圖:性能優化
經過上篇文章咱們知道了JVM中的泛型通常是經過類型擦除實現的,也就是說泛型類實例的類型實參在編譯時被擦除,在運行時是不會被保留的。基於這樣實現的作法是有歷史緣由的,最大的緣由之一是爲了兼容JDK1.5以前的版本,固然泛型類型擦除也是有好處的,在運行時丟棄了一些類型實參的信息,對於內存佔用也會減小不少。正由於泛型類型擦除緣由在業界Java的泛型又稱僞泛型。由於編譯後全部泛型的類型實參類型都會被替換Object類型或者泛型類型形參指定上界約束類的類型。例如: List<Float>、List<String>、List<Student>
在JVM運行時Float、String、Student
都被替換成Object
類型,若是是泛型定義是List<T extends Student>
那麼運行時T
被替換成Student
類型,具體能夠經過反射Erasure類可看出。app
雖然Kotlin沒有和Java同樣須要兼容舊版本的歷史緣由,可是因爲Kotlin編譯器編譯後出來的class也是要運行在和Java相同的JVM上的,JVM的泛型通常都是經過泛型擦除,因此Kotlin始終仍是邁不過泛型擦除的坎。可是Kotlin是一門有追求的語言不想再被C#那樣噴Java說什麼泛型集合連本身的類型實參都不知道,因此Kotlin藉助inline內聯函數玩了個小魔法。編程語言
泛型擦除會帶來什麼影響,這裏以Kotlin舉例,由於Java遇到的問題,Kotlin一樣須要面對。來看個例子ide
fun main(args: Array<String>) {
val list1: List<Int> = listOf(1,2,3,4)
val list2: List<String> = listOf("a","b","c","d")
println(list1)
println(list2)
}
複製代碼
上面兩個集合分別存儲了Int類型的元素和String類型的元素,可是在編譯後的class文件中的他們被替換成了List
原生類型一塊兒來看下反編譯後的java代碼函數
@Metadata(
mv = {1, 1, 11},
bv = {1, 0, 2},
k = 2,
d1 = {"\u0000\u0014\n\u0000\n\u0002\u0010\u0002\n\u0000\n\u0002\u0010\u0011\n\u0002\u0010\u000e\n\u0002\b\u0002\u001a\u0019\u0010\u0000\u001a\u00020\u00012\f\u0010\u0002\u001a\b\u0012\u0004\u0012\u00020\u00040\u0003¢\u0006\u0002\u0010\u0005¨\u0006\u0006"},
d2 = {"main", "", "args", "", "", "([Ljava/lang/String;)V", "Lambda_main"}
)
public final class GenericKtKt {
public static final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
List list1 = CollectionsKt.listOf(new Integer[]{1, 2, 3, 4});//List原生類型
List list2 = CollectionsKt.listOf(new String[]{"a", "b", "c", "d"});//List原生類型
System.out.println(list1);
System.out.println(list2);
}
}
複製代碼
咱們看到編譯後listOf函數接收的是Object類型,再也不是具體的String和Int類型了。 post
Kotlin中的is類型檢查,通常狀況不能檢測類型實參中的類型(注意是通常狀況,後面特殊狀況會細講),相似下面。性能
if(value is List<String>){...}//通常狀況下這樣的代碼不會被編譯經過
複製代碼
分析: 儘管咱們在運行時可以肯定value是一個List集合,可是卻沒法得到該集合中存儲的是哪一種類型的數據元素,這就是由於泛型類的類型實參類型被擦除,被Object類型代替或上界形參約束類型代替。可是如何去正確檢查value是否List呢?請看如下解決辦法
Java中的解決辦法: 針對上述的問題,Java有個很直接解決方式,那就是使用List原生類型。
if(value is List){...}
複製代碼
Kotlin中的解決辦法: 咱們都知道Kotlin不支持相似Java的原生類型,全部的泛型類都須要顯示指定類型實參的類型,對於上述問題,kotlin中能夠藉助星投影List<*>
(關於星投影后續會詳細講解)來解決,目前你暫且認爲它是擁有未知類型實參的泛型類型,它的做用相似Java中的List<?>
通配符。
if(value is List<*>){...}
複製代碼
特殊狀況: 咱們說is檢查通常不能檢測類型實參,可是有種特殊狀況那就是Kotlin的編譯器智能推導(不得不佩服Kotlin編譯器的智能)
fun printNumberList(collection: Collection<String>) {
if(collection is List<String>){...} //在這裏這樣寫法是合法的。
}
複製代碼
分析: Kotlin編譯器可以根據當前做用域上下文智能推導出類型實參的類型,由於collection函數參數的泛型類的類型實參就是String,因此上述例子的類型實參只能是String,若是寫成其餘的類型還會報錯呢。
在Kotlin中咱們使用as
或者as?
來進行類型轉換,注意在使用as轉換時,仍然可使用通常的泛型類型。只有該泛型類的基礎類型是正確的即便是類型實參錯誤也能正常編譯經過,可是會拋出一個警告。一塊兒來看個例子
package com.mikyou.kotlin.generic
fun main(args: Array<String>) {
printNumberList(listOf(1, 2, 3, 4, 5))//傳入List<Int>類型的數據
}
fun printNumberList(collection: Collection<*>) {
val numberList = collection as List<Int>//強轉成List<Int>
println(numberList)
}
複製代碼
運行輸出
package com.mikyou.kotlin.generic
fun main(args: Array<String>) {
printNumberList(listOf("a", "b", "c", "d"))//傳入List<String>類型的數據
}
fun printNumberList(collection: Collection<*>) {
val numberList = collection as List<Int>
//這裏強轉成List<Int>,並不會報錯,輸出正常,
//可是須要注意不能默認把類型實參當作Int來操做,由於擦除沒法肯定當前類型實參,不然有可能出現運行時異常
println(numberList)
}
複製代碼
運行輸出
若是咱們把調用地方改爲setOf(1,2,3,4,5)
fun main(args: Array<String>) {
printNumberList(setOf(1, 2, 3, 4, 5))
}
fun printNumberList(collection: Collection<*>) {
val numberList = collection as List<Int>
println(numberList)
}
複製代碼
運行輸出
分析: 仔細想下,獲得這樣的結果也很正常,咱們知道泛型的類型實參雖然在編譯期被擦除,泛型類的基礎類型不受其影響。雖然不知道List集合存儲的具體元素類型,可是確定能知道這是個List類型集合不是Set類型的集合,因此後者確定會拋異常。至於前者由於在運行時沒法肯定類型實參,可是能夠肯定基礎類型。因此只要基礎類型匹配,而類型實參沒法肯定有可能匹配有可能不匹配,Kotlin編譯採用拋出一個警告的處理。
注意: 不建議這樣的寫法容易存在安全隱患,因爲編譯器只給了個警告,並無卡死後路。一旦後面默認把它當作強轉的類型實參來操做,而調用方傳入的是基礎類型匹配而類型實參不匹配就會出問題。
package com.mikyou.kotlin.generic
fun main(args: Array<String>) {
printNumberList(listOf("a", "b", "c", "d"))
}
fun printNumberList(collection: Collection<*>) {
val numberList = collection as List<Int>
println(numberList.sum())
}
複製代碼
運行輸出
經過以上咱們知道Kotlin和Java一樣存在泛型類型擦除的問題,可是Kotlin做爲一門現代編程語言,他知道Java擦除所帶來的問題,因此開了一扇後門,就是經過inline函數保證使得泛型類的類型實參在運行時可以保留,這樣的操做Kotlin中把它稱爲實化,對應須要使用reified關鍵字。
inline fun <reified T> isInstanceOf(value: Any): Boolean = value is T
複製代碼
對於以上例子,咱們能夠說類型形參T是泛型函數isInstanceOf的實化類型參數。
咱們對inline函數應該不陌生,使用它最大一個好處就是函數調用的性能優化和提高,可是須要注意這裏使用inline函數並非由於性能的問題,而是另一個好處它能是泛型函數類型實參進行實化,在運行時能拿到類型實參的信息。至於它是怎麼實化的能夠接着往下看
咱們知道類型實化參數實際上就是Kotlin變得的一個語法魔術,那麼如今是時候揭開魔術神祕的面紗了。說實在的這個魔術能實現關鍵得益於內聯函數,沒有內聯函數那麼這個魔術就失效了。
咱們都知道內聯函數的原理,編譯器把實現內聯函數的字節碼動態插入到每次的調用點。那麼實化的原理正是基於這個機制,每次調用帶實化類型參數的函數時,編譯器都知道這次調用中做爲泛型類型實參的具體類型。因此編譯器只要在每次調用時生成對應不一樣類型實參調用的字節碼插入到調用點便可。 總之一句話很簡單,就是帶實化參數的函數每次調用都生成不一樣類型實參的字節碼,動態插入到調用點。因爲生成的字節碼的類型實參引用了具體的類型,而不是類型參數因此不會存在擦除問題。
帶實化類型參數的函數被普遍應用於Kotlin開發,特別是在一些Kotlin的官方庫中,下面就用Anko庫(簡化Android的開發kotlin官方庫)中一個精簡版的startActivity函數
inline fun <reified T: Activity> Context.startActivity(vararg params: Pair<String, Any?>) =
AnkoInternals.internalStartActivity(this, T::class.java, params)
複製代碼
經過以上例子可看出定義了一個實化類型參數T,而且它有類型形參上界約束Activity,它能夠直接將實化類型參數T當作普通類型使用
爲了好反編譯分析單獨把庫中的那個函數拷出來取了startActivityKt名字便於分析。
class SplashActivity : BizActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.biz_app_activity_welcome)
startActivityKt<AccountActivity>()//只需這樣就直接啓動了AccountActivity了,指明瞭類型形參上界約束Activity
}
}
inline fun <reified T : Activity> Context.startActivityKt(vararg params: Pair<String, Any?>) =
AnkoInternals.internalStartActivity(this, T::class.java, params)
複製代碼
編譯後關鍵代碼
//函數定義反編譯
private static final void startActivityKt(@NotNull Context $receiver, Pair... params) {
Intrinsics.reifiedOperationMarker(4, "T");
AnkoInternals.internalStartActivity($receiver, Activity.class, params);//注意點一: 因爲泛型擦除的影響,編譯後原來傳入類型實參AccountActivity被它形參上界約束Activity替換了,因此這裏證實了咱們以前的分析。
}
//函數調用點反編譯
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.setContentView(2131361821);
Pair[] params$iv = new Pair[0];
AnkoInternals.internalStartActivity(this, AccountActivity.class, params$iv);
//注意點二: 能夠看到這裏函數調用並非簡單函數調用,而是根據這次調用明確的類型實參AccountActivity.class替換定義處的Activity.class,而後生成新的字節碼插入到調用點。
}
複製代碼
讓咱們稍微在函數加點輸出就會更加清晰
class SplashActivity : BizActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.biz_app_activity_welcome)
startActivityKt<AccountActivity>()
}
}
inline fun <reified T : Activity> Context.startActivityKt(vararg params: Pair<String, Any?>) {
println("call before")
AnkoInternals.internalStartActivity(this, T::class.java, params)
println("call after")
}
複製代碼
反編譯後
private static final void startActivityKt(@NotNull Context $receiver, Pair... params) {
String var3 = "call before";
System.out.println(var3);
Intrinsics.reifiedOperationMarker(4, "T");
AnkoInternals.internalStartActivity($receiver, Activity.class, params);
var3 = "call after";
System.out.println(var3);
}
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.setContentView(2131361821);
Pair[] params$iv = new Pair[0];
String var4 = "call before";
System.out.println(var4);
AnkoInternals.internalStartActivity(this, AccountActivity.class, params$iv);//替換成確切的類型實參AccountActivity.class
var4 = "call after";
System.out.println(var4);
}
複製代碼
這裏說的使用限制主要有兩點:
明確回答Kotlin中的實化類型參數函數不能在Java中的調用,咱們能夠簡單的分析下,首先Kotlin的實化類型參數函數主要得益於inline函數的內聯功能,可是Java能夠調用普通的內聯函數可是失去了內聯功能,失去內聯功能也就意味實化操做也就化爲泡影。故重申一次Kotlin中的實化類型參數函數不能在Java中的調用
歡迎關注Kotlin開發者聯盟,這裏有最新Kotlin技術文章,每週會不按期翻譯一篇Kotlin國外技術文章。若是你也喜歡Kotlin,歡迎加入咱們~~~