Kotlin Reified 實(shí)化類型參數(shù)
1. 泛型類型擦除
我們都知道 JVM 中的泛型一般是通過類型擦除實(shí)現(xiàn)的,也就是說泛型類實(shí)例的類型實(shí)參在編譯時(shí)被擦除,在運(yùn)行時(shí)是不會(huì)被保留的。
基于這樣實(shí)現(xiàn)的做法是有歷史原因的,最大的原因之一是為了兼容JDK1.5之前的版本,當(dāng)然泛型類型擦除也是有好處的,在運(yùn)行時(shí)丟棄了一些類型實(shí)參的信息,對于內(nèi)存占用也會(huì)減少很多。
正因?yàn)榉盒皖愋筒脸蛟跇I(yè)界 Java 的泛型又稱偽泛型。因?yàn)榫?strong>譯后所有泛型的類型實(shí)參類型都會(huì)被替換為 Object 類型或者泛型類型形參指定上界約束類的類型。
例如:List<Float>、List<String>、List<Student>
在 JVM 運(yùn)行時(shí)Float、String、Student
都被替換成Object
類型,如果是泛型定義是List<T extends Student>
那么運(yùn)行時(shí)T
被替換成Student
類型,具體可以通過反射Erasure
類可看出。
雖然 Kotlin 沒有和 Java 一樣需要兼容舊版本的歷史原因,但是由于 Kotlin 編譯器編譯后出來的 class 也是要運(yùn)行在和 Java 相同的 JVM 上的,JVM的泛型一般都是通過泛型擦除,所以 Kotlin 始終還是邁不過泛型擦除的坎。
但是 Kotlin 是一門有追求的語言,不想再被 C# 那樣噴 Java 說什么泛型集合連自己的類型實(shí)參都不知道,所以 Kotlin 借助 inline 內(nèi)聯(lián)函數(shù)玩了個(gè)小魔法。
2. 泛型擦除會(huì)帶來什么影響?
泛型擦除會(huì)帶來什么影響,這里以 Kotlin 舉例,因?yàn)?Java 遇到的問題,Kotlin 同樣需要面對。看個(gè)例子:
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)
}
上面兩個(gè)集合分別存儲(chǔ)了 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 函數(shù)接收的是 Object 類型,不再是具體的 String 和 Int 類型了。
2.1 類型檢查問題
Kotlin 中的 is 類型檢查,一般情況不能檢測類型實(shí)參中的類型(注意是一般情況,后面特殊情況會(huì)細(xì)講),類似下面:
if(value is List<String>){...}//一般情況下這樣的代碼不會(huì)被編譯通過
分析:盡管我們在運(yùn)行時(shí)能夠確定 value 是一個(gè) List 集合,但是卻無法獲得該集合中存儲(chǔ)的是哪種類型的數(shù)據(jù)元素,這就是因?yàn)?strong>泛型類的類型實(shí)參類型被擦除,被 Object 類型代替或上界形參約束類型代替。但是如何去正確檢查 value 是否 List 呢?請看以下解決辦法:
Java中的解決辦法:針對上述的問題,Java 有個(gè)很直接解決方式,那就是使用 List 原生類型:
if(value is List){...}
Kotlin中的解決辦法:我們都知道 Kotlin 不支持類似 Java 的原生類型,所有的泛型類都需要顯示指定類型實(shí)參的類型,對于上述問題,Kotlin 中可以借助星投影 List<*>
(關(guān)于星投影后續(xù)會(huì)詳細(xì)講解)來解決,目前你暫且認(rèn)為它是擁有未知類型實(shí)參的泛型類型,它的作用類似 Java 中的List<?>
通配符。
if(value is List<*>){...}
特殊情況:我們說 is 檢查一般不能檢測類型實(shí)參,但是有種特殊情況那就是 Kotlin 的編譯器智能推導(dǎo)(不得不佩服Kotlin編譯器的智能):
fun printNumberList(collection: Collection<String>) {
if(collection is List<String>){...} //在這里這樣寫法是合法的。
}
分析:Kotlin 編譯器能夠根據(jù)當(dāng)前作用域上下文智能推導(dǎo)出類型實(shí)參的類型,因?yàn)?collection 函數(shù)參數(shù)的泛型類的類型實(shí)參就是 String,所以上述例子的類型實(shí)參只能是 String,如果寫成其他的類型還會(huì)報(bào)錯(cuò)呢。
2.2 類型轉(zhuǎn)換問題
在 Kotlin 中我們使用 as
或者 as?
來進(jìn)行類型轉(zhuǎn)換,注意在使用 as 轉(zhuǎn)換時(shí),仍然可以使用一般的泛型類型。只有該泛型類的基礎(chǔ)類型是正確的即使是類型實(shí)參錯(cuò)誤也能正常編譯通過,但是會(huì)拋出一個(gè)警告。一起來看個(gè)例子:
package com.mikyou.kotlin.generic
fun main(args: Array<String>) {
printNumberList(listOf(1, 2, 3, 4, 5))//傳入List<Int>類型的數(shù)據(jù)
}
fun printNumberList(collection: Collection<*>) {
val numberList = collection as List<Int>//強(qiáng)轉(zhuǎn)成List<Int>
println(numberList)
}
運(yùn)行輸出:
package com.mikyou.kotlin.generic
fun main(args: Array<String>) {
printNumberList(listOf("a", "b", "c", "d"))//傳入List<String>類型的數(shù)據(jù)
}
fun printNumberList(collection: Collection<*>) {
val numberList = collection as List<Int>
//這里強(qiáng)轉(zhuǎn)成List<Int>,并不會(huì)報(bào)錯(cuò),輸出正常,
//但是需要注意不能默認(rèn)把類型實(shí)參當(dāng)做Int來操作,因?yàn)椴脸裏o法確定當(dāng)前類型實(shí)參,否則有可能出現(xiàn)運(yùn)行時(shí)異常
println(numberList)
}
運(yùn)行輸出:
如果我們把調(diào)用的地方改成 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)
}
運(yùn)行輸出:
分析:仔細(xì)想下,得到這樣的結(jié)果也很正常,我們知道泛型的類型實(shí)參雖然在編譯期被擦除,泛型類的基礎(chǔ)類型不受其影響。雖然不知道 List 集合存儲(chǔ)的具體元素類型,但是肯定能知道這是個(gè) List 類型集合不是Set 類型的集合,所以后者肯定會(huì)拋異常。
至于前者因?yàn)樵谶\(yùn)行時(shí)無法確定類型實(shí)參,但是可以確定基礎(chǔ)類型。所以只要基礎(chǔ)類型匹配,而類型實(shí)參無法確定有可能匹配有可能不匹配,Kotlin 編譯采用拋出一個(gè)警告的處理。
注意:不建議這樣的寫法是因?yàn)槿菀状嬖诎踩[患,由于編譯器只給了個(gè)警告,并沒有卡死后路。一旦后面默認(rèn)把它當(dāng)做強(qiáng)轉(zhuǎn)的類型實(shí)參來操作,而調(diào)用方傳入的是基礎(chǔ)類型匹配而類型實(shí)參不匹配就會(huì)出問題。
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())
}
運(yùn)行輸出:
3. 什么是 reified 實(shí)化類型參數(shù)函數(shù)?
通過以上我們知道 Kotlin 和 Java 同樣存在泛型類型擦除的問題,但是 Kotlin 作為一門現(xiàn)代編程語言,他知道 Java 擦除所帶來的問題,所以開了一扇后門,就是通過 inline 函數(shù)保證使得泛型類的類型實(shí)參在運(yùn)行時(shí)能夠保留,這樣的操作 Kotlin 中把它稱為實(shí)化,對應(yīng)需要使用 reified 關(guān)鍵字。
3.1 滿足實(shí)化類型參數(shù)函數(shù)的必要條件
- 必須是 inline 內(nèi)聯(lián)函數(shù),使用 inline 關(guān)鍵字修飾;
- 泛型類定義泛型形參時(shí)必須使用 reified 關(guān)鍵字修飾;
3.2 帶實(shí)化類型參數(shù)的函數(shù)基本定義
inline fun <reified T> isInstanceOf(value: Any): Boolean = value is T
對于以上例子,我們可以說類型形參 T 是泛型函數(shù) isInstanceOf 的實(shí)化類型參數(shù)。
3.3 關(guān)于inline函數(shù)補(bǔ)充一點(diǎn)
我們對 inline 函數(shù)應(yīng)該不陌生,使用它最大一個(gè)好處就是函數(shù)調(diào)用的性能優(yōu)化和提升,但是需要注意這里使用 inline 函數(shù)并不是因?yàn)樾阅艿膯栴},而是另外一個(gè)好處它能使泛型函數(shù)類型實(shí)參進(jìn)行實(shí)化,在運(yùn)行時(shí)能拿到類型實(shí)參的信息。至于它是怎么實(shí)化的可以接著往下看。
4. 實(shí)化類型參數(shù)背后原理以及反編譯分析
我們知道類型實(shí)化參數(shù)實(shí)際上就是 Kotlin 變得的一個(gè)語法魔術(shù),那么現(xiàn)在是時(shí)候揭開魔術(shù)神秘的面紗了。說實(shí)在的這個(gè)魔術(shù)能實(shí)現(xiàn)關(guān)鍵得益于內(nèi)聯(lián)函數(shù),沒有內(nèi)聯(lián)函數(shù)那么這個(gè)魔術(shù)就失效了。
4.1 原理描述
我們都知道內(nèi)聯(lián)函數(shù)的原理,編譯器把實(shí)現(xiàn)內(nèi)聯(lián)函數(shù)的字節(jié)碼動(dòng)態(tài)插入到每次的調(diào)用點(diǎn)。那么實(shí)化的原理正是基于這個(gè)機(jī)制,每次調(diào)用帶實(shí)化類型參數(shù)的函數(shù)時(shí),編譯器都知道此次調(diào)用中作為泛型類型實(shí)參的具體類型。所以編譯器只要在每次調(diào)用時(shí)生成對應(yīng)不同類型實(shí)參調(diào)用的字節(jié)碼插入到調(diào)用點(diǎn)即可。
總之一句話很簡單,就是帶實(shí)化參數(shù)的函數(shù)每次調(diào)用都生成不同類型實(shí)參的字節(jié)碼,動(dòng)態(tài)插入到調(diào)用點(diǎn)。由于生成的字節(jié)碼的類型實(shí)參引用了具體的類型,而不是類型參數(shù)所以不會(huì)存在擦除問題。
4.2 reified 的例子
帶實(shí)化類型參數(shù)的函數(shù)被廣泛應(yīng)用于 Kotlin 開發(fā),特別是在一些 Kotlin 的官方庫中,下面就用 Anko 庫(簡化Android的開發(fā)kotlin官方庫)中一個(gè)精簡版的 startActivity 函數(shù):
inline fun <reified T: Activity> Context.startActivity(vararg params: Pair<String, Any?>) =
AnkoInternals.internalStartActivity(this, T::class.java, params)
通過以上例子可看出定義了一個(gè)實(shí)化類型參數(shù) T,并且它有類型形參上界約束 Activity,它可以直接將實(shí)化類型參數(shù)T當(dāng)做普通類型使用。
4.3 代碼反編譯分析
為了好反編譯分析單獨(dú)把庫中的那個(gè)函數(shù)拷出來取了 startActivityKt 名字便于分析。
class SplashActivity : BizActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.biz_app_activity_welcome)
startActivityKt<AccountActivity>()//只需這樣就直接啟動(dòng)了AccountActivity了,指明了類型形參上界約束Activity
}
}
inline fun <reified T : Activity> Context.startActivityKt(vararg params: Pair<String, Any?>) =
AnkoInternals.internalStartActivity(this, T::class.java, params)
編譯后關(guān)鍵代碼:
//函數(shù)定義反編譯
private static final void startActivityKt(@NotNull Context $receiver, Pair... params) {
Intrinsics.reifiedOperationMarker(4, "T");
AnkoInternals.internalStartActivity($receiver, Activity.class, params);//注意點(diǎn)一: 由于泛型擦除的影響,編譯后原來傳入類型實(shí)參AccountActivity被它形參上界約束Activity替換了,所以這里證明了我們之前的分析。
}
//函數(shù)調(diào)用點(diǎn)反編譯
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);
//注意點(diǎn)二: 可以看到這里函數(shù)調(diào)用并不是簡單函數(shù)調(diào)用,而是根據(jù)此次調(diào)用明確的類型實(shí)參AccountActivity.class替換定義處的Activity.class,然后生成新的字節(jié)碼插入到調(diào)用點(diǎn)。
}
在函數(shù)加點(diǎn)輸出就會(huì)更加清晰:
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);//替換成確切的類型實(shí)參AccountActivity.class
var4 = "call after";
System.out.println(var4);
}
5. 實(shí)化類型參數(shù)函數(shù)的使用限制
5.1 Java 調(diào)用 Kotlin 中的實(shí)化類型參數(shù)函數(shù)限制
明確回答 Kotlin 中的實(shí)化類型參數(shù)函數(shù)不能在 Java 中的調(diào)用,我們可以簡單的分析下,首先 Kotlin 的實(shí)化類型參數(shù)函數(shù)主要得益于 inline 函數(shù)的內(nèi)聯(lián)功能,但是 Java 可以調(diào)用普通的內(nèi)聯(lián)函數(shù)但是失去了內(nèi)聯(lián)功能,失去內(nèi)聯(lián)功能也就意味實(shí)化操作也就化為泡影。故重申一次 Kotlin 中的實(shí)化類型參數(shù)函數(shù)不能在 Java 中的調(diào)用。
5.2 Kotlin 實(shí)化類型參數(shù)函數(shù)的使用限制
- 不能使用非實(shí)化類型形參作為類型實(shí)參調(diào)用帶實(shí)化類型參數(shù)的函數(shù);
- 不能使用實(shí)化類型參數(shù)創(chuàng)建該類型參數(shù)的實(shí)例對象;
- 不能調(diào)用實(shí)化類型參數(shù)的伴生對象方法;
- reified關(guān)鍵字只能標(biāo)記實(shí)化類型參數(shù)的內(nèi)聯(lián)函數(shù),不能作用與類和屬性。