1. 泛型类型参数
1.1 泛型函数和属性
在使用集合的库函数都是泛型的。我们来看下slice函数的定义:
//public fun 类型形参声明 List<接收者类型形参>.slice(indices: Iterable<Int>): List<返回类型的类型形参>
public fun <T> List<T>.slice(indices: Iterable<Int>): List<T>
在一个具体的列表上调用这个函数时,可以显式地指定类型实参,但大部分情况下不必这样做,因为编译器会推导出类型。
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>
可以给类或者接口的方法、顶层函数,以及扩展函数声明类型参数。还可以声明泛型的扩展属性:
// 这个泛型扩展属性能在任何类型的元素列表上调用
val <T> List<T>.penultimate: T
get() = this[size - 2]
// 类型T会被推导为Int
println(listOf(1, 2, 3, 4).penultimate)
// 3
1.2 声明泛型类
与java一样,在kotlin中也是通过在类名称后加上一对尖括号,并把类型参数放在尖括号内来声明泛型类及泛型接口,这样就可以在类的主体内像其它类型一样使用泛型参数。
// List接口定义了类型参数 E
public interface List<out E> {
// 在类或者接口内部,E 可以当作普通的类型使用
public operator fun get(index: Int): E
// ...
}
如果一个类继承了泛型类(或者实现了泛型接口),就等为基础类型的泛型形参提供一个类型实参,它可以是具体的类型或者一个类型形参:
// 这个类实现了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并指定为父类的类型实参。
一个类还可以把它自己作为类型实参引用。
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。
1.3 类型参数约束
类型参数约束可以限制作为(泛型)类和(泛型)函数的类型实参的类型。
如果把一个类型指定为泛型类型形参的上界约束,在泛型类型具体的初始化中,其对应的类型实参就必须是这个具体类型或者它的子类型。
在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
1.4 让类型形参非空
如果你声明的是泛型类或者泛型函数,任何类型实参,包括那些可空的类型实参,都可以替换它的类型形参。
没有指定上界的类型形参将会使用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永远都是非空类型。
2. 运行时的泛型:擦除和实化类型参数
JVM上的泛型一般是通过类型擦除实现的,就是说泛型类实例的类型实参在运行时是不保留的。
2.1 运行时的泛型:类型检查和转换
与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())
}
}
2.2 声明带实化类型参数的函数
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
}
2.3 使用实化类型参数代替类引用
另一种实化类型参数的常见场景是为接收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>()
2.4 实化类型参数的限制
可以按照下面的方式使用实化类型参数:
- 用在类型检查和类型转换中(is、!is、as、as?)
- 使用kotlin反射API
- 获取相应的java.lang.Class(::class.java)
- 作为调用其他函数的类型实参
不能做下面这些事情:
- 创建指定为类型参数的类的实例
- 调用类型参数类的伴生对象的方法
- 调用带实化类型参数函数的时候使用非实化类型形参作为类型实参
- 把类、属性或者非内联函数的类型参数标记成reified
3. 变型:泛型和子类型化
变型:描述了拥有相同基础类型和不同类型实参的(泛型)类型之间是如何关联的,例如:List<String>
和List<Any>
之间如何关联。
3.1. 为什么存在变型:给函数传递实参
假设有一个接收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)
3.3 协变:保留子类型化关系
如果A是B的子类型,那么Producer<A>
就是Producer<B>
的子类型。我们说子类型化被保留了 。
在kotlin中,要声明类在某个类型参数上是可以协变的,在该类型参数的名称前加上 out 关键字即可:
// 类被声明成在T上协变
interface Producer<out T> {
fun produce(): T
}
类型参数 T 上的关键宇 out 有两层含义:
- 子类型化会被保留
Producer<Cat>
是 Producer<Animal>
的子类型
- T只能用在 out 位置
现在我们看看 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是私有的,可以使用协变。
3.4 逆变:反转子类型关系
看下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
}
如果我的文章对您有帮助,不妨点个赞鼓励一下(^_^)
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。