9.1 泛型类型参数
泛型可以让你将类型定义为类型参数。当这样的一个类的实例被创建时,类型参数就被指定的类型所代替了。和普通的类一样,Kotlin中的类型参数也能由编译器推断出来:
val authors = listOf("m1Ku","jack") //两个元素都是String,编译器推断我们创建的是List类型
但是如果我们需要一个空的集合,就需要显式的指明泛型类型,因为集合元素是空的,编译器无从推断
val list = listOf()
val list:List = listOf()
9.1.1 泛型函数和属性
如果你想写一个能够处理任何集合的函数,而不是元素是特定的类型的集合,这就需要写一个泛型函数。一个泛型函数有自己的类型参数。函数调用时,必须指明这些参数类型。
大部分处理集合的库函数都是泛型函数。以slice
函数的声明为例,这个函数返回索引在特定区间的集合
public fun List.slice(indices: IntRange): List
这是为集合定义的一个扩展函数,函数的参数类型T
被用在扩展函数的接收者类型和返回值类型中,他们都是List
。当调用这些函数时,可以显式的指定类型参数。但大多数情况下是不需要的,因为编译器可以推断其类型。
你可以在类的方法上,顶层函数以及扩展函数上声明类型参数。对于扩展函数,类型参数可以用在接收者类型和参数上。
你也可以使用相同的语法声明泛型扩展属性。例如,下面定义一个获取集合倒数第二个元素的扩展属性:
val List.preLastElement: T
get() = this[size - 2]
9.1.2 声明泛型类
就像Java中一样,Kotlin中通过在类名或者接口后面加上尖括号,并将类型参数放在尖括号中定义泛型类和接口。这样声明以后,我们就可以在类中使用这个类型参数。例如List接口的声明:
interface List{
operator fun get(index:Int):T
}
如果你的类继承了一个泛型类(或者实现了泛型接口),就必须提供一个类型参数。既可以是一个指定的类型或者是另一个类型参数:
interface BaseBean {
fun getData(): T
}
class netBean : BaseBean {
override fun getData(): String {
}
}
class infoBean:BaseBean {
override fun getData(): T {
}
}
9.1.3 类型参数的约束
类型参数约束可以限制使用在类型参数上的类型。当为一个泛型类的类型参数定义了类型约束即上界约束
,其对应的类型形参必须是指定的类型或者是他的子类型。
泛型上界约束定义语法为:
fun List.sum:T//这表明实际的类型,必须是继承自Number的
当我们为了一个类型参数定义了上界约束时,可以将泛型类型作为泛型上界的一个值使用,即可以直接调用泛型上界类的方法。
很少情况下我们需要为类型参数指定几个约束,这里使用的语法稍显不同。下面这个函数以泛型的形式保证了给定的CharSequence后面有一个点.
。这个可以使用在StringBuilder
和CharBuffer
类上
fun ensureEndPerios(seq: T)
where T : CharSequence, T : Appendable {
if (!seq.endsWith('.')) {
seq.append('.')
}
}
上面的代码,指定了类型是同时实现了CharSequence
和Appendable
接口,这就意味着对这种类型的值进行取值以及赋值操作都是可以的。
9.1.4 让类型参数非空
当你声明了一个泛型类或者函数时,任何类型包括可空类型,都是可以用来替换其类型参数的。
class Processor {
fun process(value: T) {
value?.hashCode()
}
}
如上,process函数中的value是可空的,所以使用时我们需要使用安全调用符
如果你想保证只能用非空参数来替换类型参数,可以通过给泛型添加类型上界约束来实现
class Processor {
fun process(value: T) {
value.hashCode()
}
}
约束会保证T类型总是一个非空的类型,当然也可以通过指定任何一个非空类型作为上界来保证类型参数非空。
9.2 运行时泛型:擦除和实化类型参数
就像我们已经知道的,JVM上的泛型会进行类型擦除,意味着一个泛型类实例的类型参数在运行时不会被保存。Kotlin中,可以声明一个函数为inline
的,来保证其类型不被擦除。
9.2.1 运行时的泛型:类型检查和转换
由于运行时的类型擦除,所以泛型的实例并不携带创建这个实例时的类型参数信息。例如,当你创建了一个List
,并向其中添加了一些字符串,但是在运行时你只能看到它是一个List,不能识别集合中是何种类型的元素。
如果想检查一个值是否是列表,而不是一个set或者是其他对象,可以使用星号投影语法
来实现
if (value is List<*>){
}
在这个例子中,我们是检查value值是否是一个List,仍然是不能得到元素类型的任何的信息的。
在as
和as?
转换中仍然可以使用一般的泛型类型。但是如果该类有正确的基础类型但类型实参是错误的,转换也不会失败,因为在运行时转换的时候类型实参是未知的。这样的转换只会导致编译器报“unchecked cast”警告,但是你仍然可以继续使用这个值。
Kotlin编译器是足够智能的,在编译器它已经知道相应的类型信息时,is
检查是允许的
fun printSum(c: Collection) {
if (c is List) {
println(c.sum())
}
}
这里对c
检查是否具有List
类型是可行的,因为在编译器就确定了集合包含的整形数字。
通常,Kotlin编译器负责让你知道哪些检查是危险的(禁止is
检查和发出as
转换警告),以及那些是可行的。你只需要知道这些警告的意义并且理解哪些操作符是安全的。
9.2.2 声明带实化参数的函数
当你调用一个泛型函数时,在它的函数体中,你不能确定你调用类型参数的类型。使用内联函数
我们可以避免这种限制。内联函数的类型参数可以被实化,这就意味你可以在运行时引用实际的类型参数。
fun isA(value:Any) = value is T
Error: Cannot check for instance of erased type: T
前面我们已经知道内联函数可以消除运行时开销,避免创建匿名类。这里是内联函数另外一个有用的地方:他们的类型参数可以被实化。如果标记isA
函数为内联的并且标记类型参数是reified
的,你就可以检查value是否是T
的一个实例了。
inline fun isA(value: Any) = value is T
println(isA("20"))
println(isA("20"))
>>true
false
9.2.3 使用实化类型参数代替类引用
实化类型参数一个通常的使用场景是:为使用java.lang.Class
作为参数的APIs创建适配器。举个例子,JDK中的ServiceLoader
,它接收一个代表接口或者抽象类的Class类,并返回一个实现了该接口的Service的实例。现在看一下,如何使用实化类型参数让这些APIs能更简单的被我们调用。
加载service使用ServiceLoader,加载方式如下:
val serviceImpl = ServiceLoader.load(Service::class.java)
使用::class.java
语句可以获取对应Kotlin类的java.lang.Class
。这个与Java中的Service.class是完全相同的。这个用法在后面反射的讨论中再学习。
现在用一个使用了实化参数的函数重写这个例子
val serviceImpl2 = loadService()
可以看到现在需要在类型参数中指定需要加载的Service就好了,这样的方式明显是更容易读的。
loadService
函数定义如下:
inline fun loadService(): ServiceLoader? {
return ServiceLoader.load(T::class.java)
}
9.2.4 实化类型参数的限制
即使实化类型参数是一个得力的工具,但是他们也有相应的限制。
具体来说,你可以这样使用一个实化类型参数:
- 在类型检查和转换中(
is,!is,as,as?
) - 使用Kotlin反射APIs(
::class
) - 获得对应的
java.lang.class
(::class.java) - 作为一个类型参数调用其他函数
以下场景是不能使用的:
- 创建一个已经声明为类型参数的类的实例
- 在类型参数类的伴生对象上调用函数
- 当调用一个使用实化类型参数的函数时,为其传递非实化类型参数作为类型实参
- 将类,属性得类型参数或者非内联函数标记为
reified
9.3 变型:泛型和子类型化
变型的概念描述了:拥有相同的基类和不同类型参数的类之前的关联关系,例如List
和List
。首先,讨论下为什么这些关系是重要的,然后再探究他们在Kotlin中的表现形式。
9.3.1 为什么存在变型:给函数传递一个参数
想象如果有一个需要传递List
作为参数的函数。此时传递一个List
类型的变量是安全的嘛?传递一个string给期望Any类型参数的函数肯定是安全的,因为String类继承自Any。但是当Any
和String
变成List接口的类型参数时,看起来就没那么明显易辨了。
例如,我们来考虑一个打印List的内容的函数:
fun printContents(list: List) {
println(list.joinToString())
}
printContents(listOf("jack", "m1Ku"))
>> jack,m1Ku
这看起来传递一个String的List是没问题的。函数将每个元素看作是Any,由于每个string都是Any,所以这里肯定是没问题。
现在看另外一个函数,这个函数会修改传入的集合:
fun addAnswer(list: MutableList) {
list.add(33)
}
val strings = mutableListOf("jack","m1Ku")
addAnswer(strings)
strings.maxBy { it.length }
//如果addAnswer能编译成功的话,会报以下异常
>> ClassCastException: Integer cannot be cast to String
声明了一个MutableList
类型的变量strings。然后尝试将其传给函数。如果编译器编译通过了,你就可以向字符串集合中添加一个整数,当你试图获取集合中的内容时就会导致运行时异常。正因为如此,这addAnswer(strings)
并不会编译。这个例子向我们展示了,传递一个MutableList
类型的参数给一个期望MutableList
现在就可以回答传递string的list给期望Any的list是否安全的问题了。如果函数会添加或者替换集合的元素的话,这样做就不是安全的,因为这会导致类型的不一致。反之的话,就是安全的。在Kotlin中,取决于集合是否是可变的,可以通过选择合适的接口来简单的控制。如果一个函数接受只读集合,可以传递有具体元素类型的集合。如果集合是可变的,则不能这么做。
9.3.2 类,类型和子类型
我们有时候会将类型
和类
看作相同的概念,但其实他们并不是,现在就看一看不同点。
最简单的例子是,一个非泛型类,类的名字可以直接被用作一个类型。例如,如果你写var x:String
,你定义了一个持有String类的实例的变量。但注意,相同的类名可以用来声明一个可空类型:var x:String?
。这就意味着一个Kotlin类可以用来构建至少两个类型。
对于泛型类,情况会更加复杂。为了获取一个有效的类型,你必须使用一个具体的类来替换泛型类的类型参数。List
不是一个类型,以下替换都是有效的类型:List
等等。每一个泛型类能产生无数个类型。>
为了讨论类型之间的关系,你需要熟悉子类型
这个术语。B类型
是A类型
的子类型:当需要一个A类型的值时,都可以使用B类型的值。例如,Int
是Number
的子类型,但是Int
不是String
的子类型。
例如,只可以传递一个函数参数类型的子类型的表达式给一个函数。同样的,只有当一个值的类型时一个变量类型的子类时,才可以将这个值存在这个变量中。
父类型
是子类型
的反义词:如果A是B类型的子类型,那么B就是A的父类型。
在最简单的情况下,子类型
和子类
的是完全相同的意思。例如,Int
类是Number
的子类,因此Int
类型是Number
类型的子类型。如果一个类实现了一个接口,他的类型就是这个接口类型的子类型:String是CharSequence的子类型。
可空类型
就是子类型和子类不相同的一个例子。一个非空类型
是它可空类型版本
的子类型,但他们都对应于一个类。你总是可以将非空类型的值存在可空类型的变量中。
一个泛型类-例如,MutableList
-对于任意两个不同的类A和B,MutableList
不是MutableList
子类,也不是其父类,它就称为在该类型参数上时不变的
。Java中所有的类都是不变的。
Kotlin中的List接口代表一个只读的集合。如果A是B的子类型,那么List
是List
的子类型。这样的类或者接口叫做协变的。
9.3.3 协变:保留子类型化的关系
一个协变类
是满足下述条件的一个泛型类(以Producer
为例):如果A是B的子类型那么Producer
是Producer
的子类型。我们可以将这种现象称为子类型化被保留了。
在Kotlin中,要在特定类型参数上声明类是协变的,需要在类型参数名前加上out
关键字:
interface Producer { //这个类被声明在T上是协变的
fun produce(): T
}
将一个类的类型参数标记为协变,在类型实参不能精确匹配到函数中定义的类型形参时,可以让该类的值作为这些函数的实参传递,也可以作为这些函数的返回值。