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