Kotlin Reified 實化類型參數
1. 泛型類型擦除
我們都知道 JVM 中的泛型一般是通過類型擦除實現的,也就是說泛型類實例的類型實參在編譯時被擦除,在運行時是不會被保留的。
基于這樣實現的做法是有歷史原因的,最大的原因之一是為了兼容JDK1.5之前的版本,當然泛型類型擦除也是有好處的,在運行時丟棄了一些類型實參的信息,對于內存占用也會減少很多。
正因為泛型類型擦除原因在業界 Java 的泛型又稱偽泛型。因為編譯后所有泛型的類型實參類型都會被替換為 Object 類型或者泛型類型形參指定上界約束類的類型。
例如:List<Float>、List<String>、List<Student>
在 JVM 運行時Float、String、Student
都被替換成Object
類型,如果是泛型定義是List<T extends Student>
那么運行時T
被替換成Student
類型,具體可以通過反射Erasure
類可看出。
雖然 Kotlin 沒有和 Java 一樣需要兼容舊版本的歷史原因,但是由于 Kotlin 編譯器編譯后出來的 class 也是要運行在和 Java 相同的 JVM 上的,JVM的泛型一般都是通過泛型擦除,所以 Kotlin 始終還是邁不過泛型擦除的坎。
但是 Kotlin 是一門有追求的語言,不想再被 C# 那樣噴 Java 說什么泛型集合連自己的類型實參都不知道,所以 Kotlin 借助 inline 內聯函數玩了個小魔法。
2. 泛型擦除會帶來什么影響?
泛型擦除會帶來什么影響,這里以 Kotlin 舉例,因為 Java 遇到的問題,Kotlin 同樣需要面對??磦€例子:
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 類型了。
2.1 類型檢查問題
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,如果寫成其他的類型還會報錯呢。
2.2 類型轉換問題
在 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())
}
運行輸出:
3. 什么是 reified 實化類型參數函數?
通過以上我們知道 Kotlin 和 Java 同樣存在泛型類型擦除的問題,但是 Kotlin 作為一門現代編程語言,他知道 Java 擦除所帶來的問題,所以開了一扇后門,就是通過 inline 函數保證使得泛型類的類型實參在運行時能夠保留,這樣的操作 Kotlin 中把它稱為實化,對應需要使用 reified 關鍵字。
3.1 滿足實化類型參數函數的必要條件
- 必須是 inline 內聯函數,使用 inline 關鍵字修飾;
- 泛型類定義泛型形參時必須使用 reified 關鍵字修飾;
3.2 帶實化類型參數的函數基本定義
inline fun <reified T> isInstanceOf(value: Any): Boolean = value is T
對于以上例子,我們可以說類型形參 T 是泛型函數 isInstanceOf 的實化類型參數。
3.3 關于inline函數補充一點
我們對 inline 函數應該不陌生,使用它最大一個好處就是函數調用的性能優化和提升,但是需要注意這里使用 inline 函數并不是因為性能的問題,而是另外一個好處它能使泛型函數類型實參進行實化,在運行時能拿到類型實參的信息。至于它是怎么實化的可以接著往下看。
4. 實化類型參數背后原理以及反編譯分析
我們知道類型實化參數實際上就是 Kotlin 變得的一個語法魔術,那么現在是時候揭開魔術神秘的面紗了。說實在的這個魔術能實現關鍵得益于內聯函數,沒有內聯函數那么這個魔術就失效了。
4.1 原理描述
我們都知道內聯函數的原理,編譯器把實現內聯函數的字節碼動態插入到每次的調用點。那么實化的原理正是基于這個機制,每次調用帶實化類型參數的函數時,編譯器都知道此次調用中作為泛型類型實參的具體類型。所以編譯器只要在每次調用時生成對應不同類型實參調用的字節碼插入到調用點即可。
總之一句話很簡單,就是帶實化參數的函數每次調用都生成不同類型實參的字節碼,動態插入到調用點。由于生成的字節碼的類型實參引用了具體的類型,而不是類型參數所以不會存在擦除問題。
4.2 reified 的例子
帶實化類型參數的函數被廣泛應用于 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當做普通類型使用。
4.3 代碼反編譯分析
為了好反編譯分析單獨把庫中的那個函數拷出來取了 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);
}
5. 實化類型參數函數的使用限制
5.1 Java 調用 Kotlin 中的實化類型參數函數限制
明確回答 Kotlin 中的實化類型參數函數不能在 Java 中的調用,我們可以簡單的分析下,首先 Kotlin 的實化類型參數函數主要得益于 inline 函數的內聯功能,但是 Java 可以調用普通的內聯函數但是失去了內聯功能,失去內聯功能也就意味實化操作也就化為泡影。故重申一次 Kotlin 中的實化類型參數函數不能在 Java 中的調用。
5.2 Kotlin 實化類型參數函數的使用限制
- 不能使用非實化類型形參作為類型實參調用帶實化類型參數的函數;
- 不能使用實化類型參數創建該類型參數的實例對象;
- 不能調用實化類型參數的伴生對象方法;
- reified關鍵字只能標記實化類型參數的內聯函數,不能作用與類和屬性。