在說明爲何有泛型以前,咱們先看一段代碼java
List AList = new ArrayList();
//編譯經過,運行不報錯
A.add(new B());
//編譯經過,運行報錯
A a = (A) A.get(0);
複製代碼
這段代碼,如今已經不多看到了。但實際上在Java1.5以前,這是很常常寫的代碼,也很容易犯錯的代碼。在上面的代碼中,咱們聲明瞭一個不知道儲存什麼類型的List。雖然咱們經過變量名「AList」來表明這個List是存,取A類型的集合。可是咱們仍然能夠將B類型的對象存進去。並且取出來的時候,咱們還須要進行類型強轉。這就帶來了兩個問題:編程
- 咱們沒法在儲存的時候,就限定輸入的類型。致使可能存入其餘類型致使CastClassException。
- 集合元素取出來的時候,咱們明明知道是A類型的,可是每次仍是都要進行一次強轉。
出現這問題的緣由根本在於,ArrayList()底層是使用Object[]實現的。這樣設計的本意是可讓ArrayList更加的通用,適用於一切類型。json
在瞭解了上面的需求和痛點後,咱們能夠很天然的想起泛型。它可讓類型參數化。在引入泛型後。上面的代碼咱們能夠這樣寫:安全
List<A> AList = new ArrayList();
//編譯不經過。
A.add(new B());
//再也不須要強轉
A a = A.get(0);
複製代碼
能夠看到,在引入了泛型後,在編譯時就能進行類型檢查。可是ArrayList底層實現仍是使用Object[]的,爲何能夠不用進行類型強轉呢? 咱們能夠看一下ArrayList.get()方法:bash
ArrayList.java
transient Object[] elementData;
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
E elementData(int index) {
return (E) elementData[index]; //內部進行類型強轉
}
複製代碼
到這裏,咱們總結一下引入泛型的好處:框架
其實關於泛型的背後實現,咱們在上面有說到了一些。爲了更加深入的體會他是經過類型擦除的方式來實現泛型的,咱們看一下以下代碼的字節碼:ide
//沒有加泛型
ArrayList list = new ArrayList();
//加了泛型
ArrayList<A> AList = new ArrayList();
複製代碼
字節碼:函數
//ArrayList list = new ArrayList();
0: new #2
3: dup
4: invokespecial #3
7: astore+1
//ArrayList<A> AList = new ArrayList();
8: new #2
11: dup
12: invokespecial #3
15: astore_2
複製代碼
能夠看到,ArrayList不管有沒有加泛型,它的字節碼都是同樣的。那麼它是怎麼保證咱們在一所說的泛型帶來的特性呢?其實類型檢查能夠經過編譯器檢查來實現。而類型自動轉化就如咱們上面看到的同樣,經過泛型類內部強轉實現。ui
在瞭解了泛型的實現機制之後,咱們反過來思考一下,Java爲何採用類型擦除的方式來實現泛型。答案是:向後兼容。 咱們知道向後兼容是Java一強調的一大特性,而在Java1.5以前,尚未出現泛型的時期,必然出現了大量以下代碼:this
ArrayList list = new ArrayList();
複製代碼
而類型擦除的方式實現泛型,咱們能夠看到其編譯出來的字節碼,和1.5以前的是同樣的,能夠說是徹底兼容。而後泛型的一些特性經過編譯器和對現有集合框架類的改造實現。那Kotlin號稱是能夠徹底兼容Java的,因此Kotlin的泛型實現方式固然也是和Java同樣的了。
經過上面中咱們知道,爲了是提高代碼的通用型,咱們使用泛型使類型參數化,抹去了不一樣類型帶來的差別。可是在咱們編碼過程當中,咱們時常須要在運行中獲取對象類型,而通過類型擦除的泛型類,已經失去了類型參數的信息,那麼咱們有什麼辦法能夠運行中獲取這個類型參數嗎。或許咱們能夠經過手動指定的方式獲取。具體的代碼以下:
open class A<T>(val data: T, val clazz: Class<T>) {
fun getType() {
println(clazz)
}
}
複製代碼
總結:這種方式獲取泛型類型參數不免麻煩了一點,並且它不能獲取一個獲取一個泛型類型。好比:
//編譯不一樣過,報錯
Class clazz = ArrayList<String>.class
複製代碼
那麼咱們有沒有辦法獲取一個泛型類型呢,答案是有的:
val listA = new ArryaList<A>()
val listA2 = object : ArrayList<A>(){}
println(listA.javaClass.genericSuperclass)
println(lstA2.javaClass.genericSuperclass)
//打印:
java.util.AbstractList<E>
java.util.ArrayList<java.lang.String>
複製代碼
總結:咱們發現,第二種咱們能夠獲取到list是一個什麼樣的類型。而第二種就是聲明瞭一個匿名內部類。可是爲何匿名內部類就能獲取到lis泛型參數的類型呢?其實類型擦除並非真的將所有的類型信息都擦除了,仍是會將類型信息放在對於的class的常量池中的。
因此咱們能夠嘗試設計出獲取全部類型信息的泛型類。
open class GenericsToken<T> {
var type: Type = Any::class.java
init {
val superClass = this.javaClass.gnericSuperclass
type = superClass as ParameterizedType).getActualTypeArguments()[0]
}
}
fun test() {
val gt = object : GenericsToken<Map<String, String>>(){}
println(gt.type)
}
//打印結果
java.util.Map<java.lang.String, ? extends java.lang.String>
複製代碼
總結:匿名內部類在初始化的時候,綁定父類或父類接口的相應信息,這樣能夠經過獲取父類或父藉口的父接口的泛型類型信息來獲取咱們想要的泛型類型。其實經常使用的Gson框架也是採用這樣的方式獲取的。
val json = new Json("...")
val type = object : TypeToken<List<String>>(){}.type
val stringList = Gson().fromJson<List<String>>(json.type)
複製代碼
咱們知道Kotlin的內聯函數是在編譯的時候,編譯器把內聯函數的字節碼直接插入到調用的地方,因此參數類型也會被插入到字節碼中。而在內聯函數中獲取泛型的參數類型也很是簡單,只須要加上reified關鍵字就能夠。
inline fun <reified T> getType(): T {
return T::class.java
}
複製代碼
咱們前面說的泛型時,講到其中一個特性就是類型安全,其實也就是說泛型自己帶有類型的約束力。那麼這裏講的類型約束是什麼意思呢。其實就是對泛型的約束。在Java中看咱們會看到以下代碼:
class Test<T extends B> {
...
}
複製代碼
經過在T後面加了extends B約束了這個泛型必須是B的子類。那麼在Kotlin中,繼承是用:表示的,因此Kotlin的泛型約束以下:
class Test<T: B>{
}
複製代碼
可是,若是咱們須要多個約束呢?在Kotlin中可使用 where 關鍵字來實現這個需求以下:
class Test<T> where T: A, T: B{
}
複製代碼
利用where關鍵字,咱們能夠約束泛型T必須是A和B的子類。
講義:若是類型A是類型B的子類型,那麼Generic<A>也是Generic<B>的子類,這就是協變。
在kotlin中,咱們要實現這種關係,能夠經過在泛型類或者泛型方法的泛型參數前面加 out 關鍵字。以下:
//定義實體類關係
open class Flower
class WhiteFlower: Flower(){}
class ReaFlower: Flower(){}
//生產者
interface Product<out T> {
fun produce(): T
}
class WhiteFlowerProduct<WhiteFlower> {
//將泛型類型做爲返回
override fun produce(): WhiteFlower {
return WhileFlower();
}
}
//以下編譯經過
val product: Product<Flower> = WhiteFLowerProduct()
複製代碼
總結:能夠看到,WhiteFLowerProduct()能夠賦值給Product 類型變量,就是由於經過out指明瞭協變關係。並且咱們也看到,泛型類型作爲返回類型,被生產出來。那麼若是咱們添加一個泛型類型的對象呢?以下:
interface Product<out T> {
fun produce(): T
//編譯器報錯
fun add(t: T)
}
class WhiteFlowerProduct<WhiteFlower> {
//將泛型類型做爲返回
override fun produce(): WhiteFlower {
return WhileFlower();
}
override fun add(flower: WhiteFlower){
return WhileFlower();
}
}
複製代碼
結果是編譯器報錯:Type parameter T is declare as 'out' but occurs in 'in' position in type T。翻譯過來就是被聲明爲out的類型T不能出如今輸入的位置。其實咱們經過'out'關鍵字也能夠知道,被其修飾的泛型只能做爲生產者輸出,而不能做爲消費者輸入。因此'out'修飾的泛型經常做爲方法的返回而使用。這就是協變帶來的限制。那麼協變爲何不能輸入呢。咱們能夠採用反證法來理解:假如能夠添加,那麼會發生什麼事?
val flowerProduct: Product<Flower> = WhiteFLowerProduct()
//編譯不出錯,可是運行時會出現類型不兼容錯誤。
flowerProduct.add(ReaFlower())
複製代碼
其在Java中,相對應的泛型協變咱們是這樣定義的:<? extends Object> 可是這一不便理解的泛型協變定義在Kotlin上被改進成用out關鍵字,更加能體現其協變只讀不可寫的特性。
定義:若是類型A是類型B的子類型,反過來Generic<B>是Generic<A>的子類型,咱們稱這種關係爲逆變。在Kotlin中,咱們用'in'關鍵字來聲明逆變泛型。以下例子:
val numberComparator = Comparator<Number> {
n1, n2 -> n1.toDouble.compareTo(n2.toDouble())
}
val daoubleList = mutableListOf(2.0, 3.0)
//針對Double數據類型,咱們使用Number類型的Comparator
doubleList.sortWith(numberComparator)
val intList = mutableListof(1, 2)
//針對Int數據類型,咱們仍然使用Number 類型的Comparator
intList.sortWith(numberComparator)
//能夠看到這裏對泛型T,使用了in關鍵字。
public fun <T> MutableList<T>.sortWith(comparator: Comparator<in T>): Unit {
if (size > 1) java.util.Collections.sort(this, comparator)
}
複製代碼
經過如上代碼咱們知道,原本Double和Int是Number的子類,經過in修飾符後,Comparator成爲了Comparator 和 Comparator 的子類,因此能夠將Comparator賦值給Comparator和Comparator。從而不用在專門根據不一樣的數據類型,定義不一樣的DoubleComparator、IntComparatort等。 一樣的,經過它的名字'in'也能夠知道。in修飾的泛型只能做爲輸入類型,而不能做爲返回類型。在Java中它對應着<? super T>。
是 | 協變 | 逆變 | 不變 |
---|---|---|---|
Kotlin | 實現方式: 只能做爲消費者,只能讀取不能寫入 | 實現方式 只能添加,讀取受限 | 實現方式:, 可讀可寫 |
Java | 實現方式:<? extends T> 只能做爲消費者,只能讀取不能寫入 | 實現方式<? super T> 只能添加,讀取受限 | 實現方式:, 可讀可寫 |
小弟早期閱讀《Kotlin核心編程》時,一直以爲書中對Kotlin的泛型講解得很是好,因此一直有想法寫一篇相關的博文,也算是讀書筆記了。文中有不盡之處,歡迎留言指出。謝謝!