博客主頁java
在使用集合的庫函數都是泛型的。咱們來看下slice函數的定義:segmentfault
//public fun 類型形參聲明 List<接收者類型形參>.slice(indices: Iterable<Int>): List<返回類型的類型形參> public fun <T> List<T>.slice(indices: Iterable<Int>): List<T>
在一個具體的列表上調用這個函數時,能夠顯式地指定類型實參,但大部分狀況下沒必要這樣作,由於編譯器會推導出類型。api
val letters = ('a'..'z').toList() //顯式地指定類型實參 println(letters.slice<Char>(0..2)) // [a, b, c] // 編譯器推導出這裏的T是Char println(letters.slice(10..13)) // [k, l, m, n]
先來看下filter函數的聲明,它接收一個函數類型:(T) -> Boolean的參數安全
val authors = listOf("Dmitry", "Svetlana") val readers = mutableListOf<String>("Bob", "Svetlana") // filter函數的聲明 public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> readers.filter { it !in authors }
編譯器推斷T就是String,由於它知道函數應該在List<T>
上調用,而它的接收者readers的真實類型是List<String>
ide
能夠給類或者接口的方法、頂層函數,以及擴展函數聲明類型參數。還能夠聲明泛型的擴展屬性:函數
// 這個泛型擴展屬性能在任何類型的元素列表上調用 val <T> List<T>.penultimate: T get() = this[size - 2] // 類型T會被推導爲Int println(listOf(1, 2, 3, 4).penultimate) // 3
與java同樣,在kotlin中也是經過在類名稱後加上一對尖括號,並把類型參數放在尖括號內來聲明泛型類及泛型接口,這樣就能夠在類的主體內像其它類型同樣使用泛型參數。性能
// List接口定義了類型參數 E public interface List<out E> { // 在類或者接口內部,E 能夠看成普通的類型使用 public operator fun get(index: Int): E // ... }
若是一個類繼承了泛型類(或者實現了泛型接口),就等爲基礎類型的泛型形參提供一個類型實參,它能夠是具體的類型或者一個類型形參:this
// 這個類實現了List,並提供了具體類型實參 String class StringList : List<String> { override fun get(index: Int): String = ... } // ArrayList的泛型類型形參 T 就是List的類型實參 class ArrayList<T> : List<T> { override fun get(index: Int): T = ... }
StringList類被聲明只能包含String元素,而類ArrayList指定了本身的類型參數T並指定爲父類的類型實參。spa
一個類還能夠把它本身做爲類型實參引用。code
public interface Comparable<in T> { public operator fun compareTo(other: T): Int } public class String : Comparable<String> { public override fun compareTo(other: String): Int }
String類實現了Comparable泛型接口,提供類型String給類型實參T。
類型參數約束能夠限制做爲(泛型)類和(泛型)函數的類型實參的類型。
若是把一個類型指定爲泛型類型形參的上界約束,在泛型類型具體的初始化中,其對應的類型實參就必須是這個具體類型或者它的子類型。
在java中,使用extends關鍵字:
<T extends Number> T sum(List<T> list)
在kotlin中,使用冒號(:)
// 經過在類型參數後指定上界來定義約束 // <類型參數 : 上界> fun <T : Number> List<T>.sum(): T
若是指定了類型形參T的上界,就能夠把類型T的值看成它的上界(類型)的值使用:
// 指定Number爲類型形參的上界 fun <T : Number> oneHalf(value: T): Double { // 調用Number類中的方法 return value.toDouble() / 2.0f } println(oneHalf(3)) // 1.5
再來看一個例子:T的上界是泛型類型Comparable<T>
,String類繼承了Comparable<String>
,String就能夠做爲max函數的有效類型實參。
// 聲明帶類型參數約束的函數 fun <T : Comparable<T>> max(first: T, second: T): T { // 根據kotlin運算符約定會被編譯成first.compareTo(second) > 0 return if (first > second) first else second } println(max("kotlin", "java")) // kotlin
若是你聲明的是泛型類或者泛型函數,任何類型實參,包括那些可空的類型實參,均可以替換它的類型形參。
沒有指定上界的類型形參將會使用Any?這個默認的上界。
class Processor<T> { fun process(value: T) { // "value"是可空的,因此要用安全調用 value?.hashCode() } } // 可空類型String?被用來替換T val nullableStringProcessor = Processor<String?>() // 使用null做爲value實參的代碼能夠編譯 nullableStringProcessor.process(null)
若是你想保證替換類型形參始終是非空類型,能夠經過指定一個約束來實現。若是除了可空性以外沒有任何限制,可使用Any代替默認的Any?做爲上界:
// 指定非空上界 class Processor<T : Any> { fun process(value: T) { // 類型T的值如今是非空的 value.hashCode() } }
約束<T : Any>
確保了類型T永遠都是非空類型。
JVM上的泛型通常是經過類型擦除實現的,就是說泛型類實例的類型實參在運行時是不保留的。
與java同樣,kotlin的泛型在運行時也被擦除了。例如:建立一個List<String>
,在運行時只能看到它是一個List。
注意一點:擦除泛型類型信息是有好處的,應用程序使用的內存總量較小,由於要保存在內存中的類信息更少。
可使用特殊的星號投影語法來檢查,一個值是不是列表,而不是set或者其餘對象。
if (value is List<*>) { ... }
能夠認爲它就是擁有未知類型實參的泛型類型(或者類比於java的List<?>)。
as和as?轉換中可使用通常的泛型類型。若是該類有正確的基礎類型但類型實參是錯誤的,轉換也不會失敗,由於在運行時轉換髮生的時候類型實參是未知的,這樣寫,編譯器會發出Unchecked cast(未受檢轉換)的警告
// 對泛型類型作類型轉換 fun printSum(c: Collection<*>) { // 這裏會有警告 Unchecked cast:List<*> to List<Int> val intList = c as? List<Int> ?: throw IllegalArgumentException("List is expected") println(intList.sum()) } printSum(listOf(1, 2, 3)) // 6 printSum(listOf("a", "b", "c")) // java.lang.ClassCastException: // java.lang.String cannot be cast to java.lang.Number
若是傳一個錯誤類型的值,運行時就會拋出ClassCastException異常。
// 對已知類型實參作類型轉換 fun printSum(c: Collection<Int>) { if (c is List<Int>) { println(c.sum()) } }
kotlin泛型在運行時會被擦除。在調用泛型函數的時候,在函數體中不能決定調用它用的類型實參:
// Error: Cannot check for instance of erased type: T fun <T> isA(value: Any) = value is T
只有一種例外能夠避免這種限制:內聯函數。內聯函數的類型形參可以被實化,意味着能夠在運行時引用實際的類型實參。
若是把isA函數聲明成inline並用reified標記類型參數,就能夠用該函數檢查value是否是T實例:
// 聲明帶實化類型參數的函數 // 如今代碼能夠編譯了 inline fun <reified T> isA(value: Any) = value is T
一個實化類型參數能發揮做用的最簡單的例子就是標準庫函數filterIsInstance。這個函數接收一個集合,選擇其中那些指定類的實例,而後返回這些選中的實例。
// 使用標準庫函數filterIsInstance val list = listOf("one", 2, "three") println(list.filterIsInstance<String>()) // [one, three]
經過指定<String>
做爲函數的類型實參,代表只關心字符串,因此函數的返回類型是List<String>
。
這種狀況下,類型實參在運行時是已知的,函數filterIsInstance使用它來檢查列表中的值是否是指定爲該類型實參的類的實例。
// filterIsInstance函數的定義 // 「reified」聲明瞭類型參數不會在運行時被擦除 public inline fun <reified R> Iterable<*>.filterIsInstance(): List<@kotlin.internal.NoInfer R> { return filterIsInstanceTo(ArrayList<R>()) } public inline fun <reified R, C : MutableCollection<in R>> Iterable<*>.filterIsInstanceTo(destination: C): C { // 能夠檢查元素是否是指定爲類型實參的類的實例 for (element in this) if (element is R) destination.add(element) return destination }
另外一種實化類型參數的常見場景是爲接收java.lang.Class類型參數的API構建適配器。
如:jdk中的ServiceLoader,它接收一個表明接口或者抽象類的java.lang.Class,並返回實現了該接口(或者繼承了該抽象類)的類的實例。
// 使用標準的ServiceLoader java api加載一個服務 val serviceImpl = ServiceLoader.load(Service::class.java)
::class.java獲取java.lang.Class對應的kotlin類,這和java中的Service.class是徹底等同的。
接下來用帶實化類型參數的函數重寫這個例子:
// loadService函數定義 // 類型參數標記成了reified inline fun <reified T> loadService(): ServiceLoader<T> { // 把T::class當成類型形參的類訪問 return ServiceLoader.load(T::class.java) } val serviceImpl = loadService<Service>()
能夠簡化Android上的startActivity函數:
// 可使用實化類型參數來代替傳遞做爲java.lang.Class的activity類 inline fun <reified T : Activity> Context.startActivity() { // 把T:class當成類型參數的類訪問 val intent = Intent(this, T:class.java) startActivity(intent) } // 調用方法 startActivity<DetailActivity>()
能夠按照下面的方式使用實化類型參數:
不能作下面這些事情:
變型:描述了擁有相同基礎類型和不一樣類型實參的(泛型)類型之間是如何關聯的,例如:List<String>
和List<Any>
之間如何關聯。
假設有一個接收List<Any>
做爲實參的函數,把List<String>
類型的變量傳給這個函數是否安全?
若是函數添加或者替換了列表中的元素是不安全的,由於這樣會產生類型不一致的可能性,不然它就是安全的。
下面這段代碼是安全的,由於String類繼承了Any,因此是安全的。
fun printContents(list: List<Any>) { println(list.joinToString()) } printContents(listOf("abc", "cbd")) // abc, cbd
看另外一個函數,它修改列表(接收一個MutableList做爲參數):
fun addAnswer(list: MutableList<Any>) { list.add(23) }
而後把一個字符串列表傳給這個函數,將發生什麼呢?
val list = mutableListOf("abc", "cbd") // type mismatch,編譯通不過 addAnswer(list)
若是A是B的子類型,那麼Producer<A>
就是Producer<B>
的子類型。咱們說子類型化被保留了 。
在kotlin中,要聲明類在某個類型參數上是能夠協變的,在該類型參數的名稱前加上 out 關鍵字便可:
// 類被聲明成在T上協變 interface Producer<out T> { fun produce(): T }
類型參數 T 上的關鍵宇 out 有兩層含義:
Producer<Cat>
是 Producer<Animal>
的子類型
如今咱們看看 List<Interface>
接口。kotlin中的List是隻讀的,因此它只有一個返回類型爲 T 的元素的方法 get ,而沒有定義任何把類型爲 T 的元素存儲到列表中的方法。所以,它也是協變的。
// kotlin標準庫中List的定義 public interface List<out E> : Collection<E> { // 只讀接口只定義了返回 E 的方法。因此E在 」out「 位置 public operator fun get(index: Int): E // .... }
類型形參不光能夠直接看成參數類型或者返回類型使用,還能夠看成另外一個類型的類型實參。例如, List接口就包含了一個返回 List<T>
的 subList方法。
public interface List<out E> : Collection<E> { // 這裏的 E 也在 」out「位置 public fun subList(fromIndex: Int, toIndex: Int): List<E> }
不能把MutableList<E>
在它的類型參數上聲明成協變的,由於它既含有接收類型爲E的值做爲參數的方法,也含有返回這種值的方法(所以,E 出現 in 和 out 兩種位置上)。
// MutableList不能在E上聲明成協變的 public interface MutableList<E> : List<E>, MutableCollection<E> { // 由於E用在了 「in」 位置 override fun add(element: E): Boolean }
其中構造方法的參數既不在 in 位置,也不在 out 位置。
若是你在構造方法的參數上使用了關鍵字 val 和 var,同時就會聲明一個getter 和setter(若是屬性是可變的)。所以,對只讀屬性來講,類型參數用在了 out 位置,而可變屬性在 out 位置 in 位置都使用了它。
class Herd<T: Animal>(var leadAnimal: T, vararg animals: T) { .. . }
上面這個例子中, T 不能用 out 標記,由於類包含屬性 leadAnimal 的setter ,它在 in 位置用到了 T。注意:私有方法的參數既不在 in 位置也不在 out 位置。若是leadAnimal是私有的,可使用協變。
看下Comparator接口:
interface Comparator<in T> { // 在 「in」 位置使用了 T fun compare(el: T, e2: T): Int { ... } }
在類型參數T上的in關鍵字意味着子類型化被反轉了,並且 T 只能用在 in 位置。
一個類能夠在一個類型參數上協變,同時在另一個類型參數上逆變。
interface Functionl<in P, out R> { operator fun invoke (p: P) : R }
若是個人文章對您有幫助,不妨點個贊鼓勵一下(^_^)