博客主页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 }
若是个人文章对您有帮助,不妨点个赞鼓励一下(^_^)