說kotlin中這個關(guān)鍵字之前先簡單說下Java中的泛型,我們在編程中足淆,出于復(fù)用和高效的目的尝胆,經(jīng)常使用泛型丧裁。泛型是通過在JVM底層采取類型擦除的機制實現(xiàn)的,Kotlin也是這樣含衔。
泛型
泛型是 Java SE 1.5 中的才有的特性煎娇,泛型的本質(zhì)是參數(shù)化類型,可分為泛型類贪染、泛型接口缓呛、泛型方法。在沒有泛型的情況的下只能通過對Object 的引用來實現(xiàn)參數(shù)的任意化杭隙,帶來的缺點就是要顯式的強制類型轉(zhuǎn)換哟绊,而強制轉(zhuǎn)換在編譯期是不做檢查的,容易把問題留到運行時痰憎,所以泛型的好處是在編譯時檢查類型安全票髓,并且所有的強制轉(zhuǎn)換都是自動和隱式的,提高了代碼的重用率铣耘,避免在運行時出現(xiàn) ClassCastException洽沟。
JDK 1.5 中引入了泛型來允許強類型在編譯時進行類型檢查;JDK 1.7 中泛型實例化類型具備了自動推斷的能力蜗细,譬如List<String> mList = new ArrayList<String>()
可以寫成 List<String> mList = new ArrayList<>()
類型擦除
泛型通過類型擦來實現(xiàn)裆操,編譯器在編譯時擦除所有泛型類型相關(guān)信息,即運行時就不存在任何泛型類型相關(guān)的信息炉媒,譬如 List<Integer>
在運行時僅用一個 List 來表示踪区,這樣做的目的是為了和 Java 1.5 之前版本進行兼容。
fun test() {
val mList= ArrayList<String>()
mList.add("123")
Log.v("tag",mList[0])
}
字節(jié)碼如下:
public final test()V
L0
LINENUMBER 18 L0
NEW java/util/ArrayList
DUP
INVOKESPECIAL java/util/ArrayList.<init> ()V
ASTORE 1
L1
LINENUMBER 19 L1
ALOAD 1
LDC "123"
INVOKEVIRTUAL java/util/ArrayList.add (Ljava/lang/Object;)Z
POP
L2
LINENUMBER 20 L2
LDC "tag"
ALOAD 1
ICONST_0
INVOKEVIRTUAL java/util/ArrayList.get (I)Ljava/lang/Object;
CHECKCAST java/lang/String
INVOKESTATIC android/util/Log.v (Ljava/lang/String;Ljava/lang/String;)I
POP
L3
LINENUMBER 21 L3
RETURN
L4
LOCALVARIABLE mList Ljava/util/ArrayList; L1 L4 1
LOCALVARIABLE this Lcom/github/coroutinesdemo/Test; L0 L4 0
MAXSTACK = 3
MAXLOCALS = 2
INVOKEVIRTUAL java/util/ArrayList.add (Ljava/lang/Object;)Z
list.add("123")
實際上是"123"
作為Object
存入集合中的
INVOKEVIRTUAL java/util/ArrayList.get (I)Ljava/lang/Object
從list
實例中讀取出來Object
然后轉(zhuǎn)換成String
之后才能使用
CHECKCAST java/lang/String
進行類型轉(zhuǎn)換
泛型擦除在編譯成字節(jié)碼時首先進行類型檢查橱野,再進行類型擦除(即所有類型參數(shù)都用限定類型替換朽缴,包括類、變量和方法如果類型變量有限定則原始類型就用第一個邊界的類型來替換水援,譬如 class Test<T extends Comparable & Serializable> {} 的原始類型就是 Comparable)
如果類型擦除和多態(tài)性發(fā)生沖突時就在子類中生成橋方法解決密强,接著如果調(diào)用泛型方法的返回類型被擦除則在調(diào)用該方法時插入強制類型轉(zhuǎn)換茅郎。
類型擦除的問題
類型擦除會有一系列的問題,這里不展開了
泛型讀取時會進行自動類型轉(zhuǎn)換問題或渤,所以如果調(diào)用泛型方法的返回類型被擦除則在調(diào)用該方法時插入強制類型轉(zhuǎn)換
泛型類型參數(shù)不能是基本類型, 擦除后的Object 是引用類型不是基本類型
無法進行具體泛型參數(shù)類型的運行時類型檢查系冗,
instanceof ArrayList<?>
不能拋出也不能捕獲泛型類的對象,因為異常是在運行時捕獲和拋出的薪鹦,而在編譯時泛型信息會被擦除掌敬,擦除后兩個 catch 會變成一樣的東西。不能在 catch 子句中使用泛型變量池磁,因為泛型信息在編譯時已經(jīng)替換為原始類型(譬如 catch(T) 在限定符情況下會變?yōu)樵碱愋?Throwable)奔害,如果可以在 catch 子句中使用,則違背了異常的捕獲優(yōu)先級順序
fun <T>Int.toCase():T?{
return (this as T)
}
上述代碼在轉(zhuǎn)換類型時地熄,沒有進行檢查华临,所以有可能會導(dǎo)致運行時崩潰,編譯器會提示unchecked cast
警告,如果獲得的數(shù)據(jù)不是它期望的類型端考,這個函數(shù)會出現(xiàn)崩潰
fun testCase() {
1.toCase<String>()?.substring(0)
}
這就會出現(xiàn)TypeCastException錯誤雅潭,所以為了安全獲取數(shù)據(jù)一般都是需要顯式傳遞class信息:
fun <T> Int.toCase(clz:Class<T>):T?{
return if (clz.isInstance(this)){
this as? T
}else{
null
}
}
fun testCase() {
1.toCase(String::class.java)?.substring(0)
}
但這需要通過顯示傳遞class的方式過于麻煩繁瑣尤其是傳遞多類型參數(shù),基于類型擦除機制無法在運行時得到T的類型信息却特,所以用到安全轉(zhuǎn)換操作符as或者as?
fun <T> Bundle.putCase(key: String, value: T, clz:Class<T>){
when(clz){
Long::class.java -> putLong(key,value as Long)
String::class.java -> putString(key, value as String)
Char::class.java -> putChar(key, value as Char)
Int::class.java -> putInt(key, value as Int)
else -> throw IllegalStateException("Type not supported")
}
}
那有沒有排除這種傳遞參數(shù)之外的優(yōu)雅實現(xiàn)扶供??裂明?
reified 關(guān)鍵字
reified關(guān)鍵字的使用很簡單:
在泛型類型前面增加
reified
修飾-
在方法前面增加
inline
改進上述代碼
inline fun <reified T> Int.toCase():T?{ return if (this is T) { this } else { null } }
testCase()方法調(diào)用轉(zhuǎn)成Java 代碼看下 :
public final void testCase() {
int $this$toCase$iv = 1;
int $i$f$toCase = false;
String var10000 = (String)(Integer.valueOf($this$toCase$iv) instanceof String ? Integer.valueOf($this$toCase$iv) : null);
// inline部分
String var1;
if (var10000 != null) {
// 替換開始
var1 = var10000;
$this$toCase$iv = 0;
if (var1 == null) {
throw new TypeCastException("null cannot be cast to non-null type java.lang.String");
}
var10000 = var1.substring($this$toCase$iv);
Intrinsics.checkExpressionValueIsNotNull(var10000, "(this as java.lang.String).substring(startIndex)");
} else {
var10000 = null;
}
// reified替換結(jié)束
var1 = var10000;
System.out.println(var1);
}
Inline的作用這里不再多說了椿浓,noinline和crossinline又是啥?這里可以看下漾岳。
泛型在運行時會被類型擦除轰绵,但是在inline函數(shù)中我們可以指定類型不被擦除, 因為inline函數(shù)在編譯期會將字節(jié)碼copy到調(diào)用它的方法里尼荆,所以編譯器會知道當(dāng)前的方法中泛型對應(yīng)的具體類型是什么左腔,然后把泛型替換為具體類型,從而達到不被擦除的目的捅儒,在inline函數(shù)中我們可以通過reified關(guān)鍵字來標(biāo)記這個泛型在編譯時替換成具體類型
示例
我們在用Gson解析json數(shù)據(jù)的時候液样,是如何解析數(shù)據(jù)拿到泛型類型 Bean
結(jié)構(gòu)的?TypeToken 是一種方案巧还,可以通過getType() 方法獲取到我們使用的泛型類的泛型參數(shù)類型,不過采用反射解析的時候鞭莽,Gson構(gòu)造對象實例時調(diào)用的是默認(rèn)無參構(gòu)造方法,所以依賴 Java 的 Class 字節(jié)碼中存儲的泛型參數(shù)信息麸祷,Java 的泛型機制雖然在編譯期間進行了擦除澎怒,但是Java 在編譯時會在字節(jié)碼里指令集以外的地方保留部分泛型的信息,接口阶牍、類喷面、方法定義上的所有泛型星瘾、成員變量聲明處的泛型都會被保留類型信息,其他地方的泛型信息都會被擦除惧辈,這些信息被保存在 class 字節(jié)碼的常量池中琳状,使用泛型的代碼處會生成一個 signature 簽名字段浴栽,通過簽名 signature 字段指明這個常量池的地址煤痕,JDK 提供了方法去讀取這些泛型信息的方法,利用反射就可以獲得泛型參數(shù)的具體類型碗脊,譬如:
(mList.javaClass.genericSuperclass as ParameterizedType).actualTypeArguments[0]
一般Gson解析:
inline fun <reified T> Gson.fromJson(jsonStr: String) =
fromJson(json, T::class.java)
如果用Moshi解析:
inline fun <reified T> Moshi.fromJson(jsonStr: String) = Moshi.Builder().add(KotlinJsonAdapterFactory()).build().adapter(T::class.java).fromJson(jsonStr)