《Kotlin实战》-第09章:泛型
第九章 泛型
泛型和变型一般被认为是Java语言中最难处理的部分,Kotlin中也是。
前两节还算简单,讲解泛型的相关知识。第三节讲解变型,会是本书到目前最难理解的部分,概念多,比较难懂,区分有难度。建议先看懂概念原理,后面再多实践多总结。
虽然Kotlin在设计时考虑了和Java的互操,但建议在看本章时,先把Java的那一套通配符知识放在一边,专注于Kotlin自身的设计逻辑和语言表达。
9.1 泛型类型参数
泛型允许定义带类型形参的类型。当这种类型的实例被创建出来的时候,类型形参被替换成称为类型实参的具体类型。
和一般类型一样,Kotlin编译器常可以推导出类型实参。
val authors = listOf("aa","bb")
对于空列表来说,需要显式地指定类型形参。有两种方式,一是在变量声明中,一是在创建函数中说明。
val readers:MutableList<String> = mutableListOf()
val readers = mutableListOf<String>()
特别地,Java由于在1.5版本后才支持泛型,为了兼容性,可以使用没有类型参数的泛型类型。Kotlin则不允许。
9.1.1 泛型函数和属性
1.泛型函数
泛型函数就是有自己的类型形参,在每次函数调用的时候都必须替换成具体的类型实参。
大部分使用集合的库函数都是泛型的。
示例:
//泛型函数声明
fun <T> List<T>.slice(indices:IntRange):List<T>
//调用
val letters = ('a' .. 'z').toList()
println(letters.slice<Char>(0..2))
println(letters.slice(10..13))
>>>>>
[a,b,c]
[k.l.m.n]
可以声明类型参数的函数有:
- 类或接口的方法
- 顶层函数
- 扩展函数
- 带接受者和lambda参数的类型
2.属性
可以声明扩展属性为泛型,但不能声明普通属性为泛型,因为不能在一个类的属性中存储多个不同类型的值。
示例:
//声明扩展属性为泛型
val <T> List<T>.penultimate:T
get() = this[size - 2]
//调用
println(listOf(1,2,3,4).penultimate)
>>>>
3
//声明普通属性会报错
//val <T> x:T = TODO()
9.1.2 声明泛型类
Kotlin声明泛型类或接口的方式和Java一样,用尖括号加类型参数来声明。
下面是一个简单的示例,后面章节会加以改进:
interface List<T> {
//在接口内部把声明的泛型当做普通类型使用了
operator fun get(index:Int):T
//....
}
当继承了泛型类,或者实现了泛型接口,需要为父类的泛型形参提供一个类型实参,当然这个类型实参可以是具体类型或另一个类型形参。
//接口的泛型形参被提供了具体类型
class StringList:List<String>{
override fun get(index:Int):String = .....
}
//接口的泛型形参被提供了另一个泛型形参
class ArrayList<T>:List<T>{
override fun get(index:Int):T = .....
}
特别地,一个类可以把自己作为类型实参引用,经典例子是Comparable接口
interface Compareable<T>{
fun compareTo(other:T):Int
}
//String实现了接口,同时把自己作为了接口泛型的类型实参
class String:Comparable<String>{
override fun compareTo(other:String):Int = .....
}
9.1.3 类型参数约束
类型参数约束可以限制作为泛型类和泛型函数的类型实参的类型。
本节介绍其中一种约束:上界约束。
- 当一个类型被指定为泛型类型形参的上界约束,意味着泛型类型的实参必须是这个具体类型或者它的子类型。
- 书写形式是泛型类型后+冒号+上界约束。
//声明泛型及上界
fun <T:Number> List<T>.sum():T
//调用
println(listOf(1,2,3).sum())
- 指定了类型形参T的上界后,可以把类型T当做它的上界类型使用,例如调用它的方法。
- 可以在一个类型参数上指定多个约束。书写形式是where + 类型参数:上界约束 + , + 类型参数:上界约束
fun <T> ensureTrailingPeriod(seq:T) where T:CharSequence, T:Appendable{
//下面分别使用了两个上界约束里的方法
if(!seq.endsWith('.')){
seq.append('.')
}
}
9.1.4 让类型形参非空
没有指定上界的类型形参默认使用Any?为上界,也就意味着是可空类型,调用一些方法时需要用安全调用。
示例:
class Processor<T>{
fun process(value:T){
value?.hashCode()
}
}
如果想确保为非空,可以指定任意非空类型为上界,如Any。
class Processor<T:Any>{
fun process(value:T){
value.hashCode()
}
}
9.2 运行时的泛型:擦除和实化类型参数
JVM上的泛型一般通过类型擦除实现,即泛型类实例的类型实参在运行时是不保留的。
Kotlin中的泛型一般也会在运行时被擦除,但是可以通过内联函数和实化类型参数来解决这个问题。
9.2.1 运行时的泛型:类型检查和转换
Kotlin的泛型和Java一样,在运行时也被擦除了,也就是泛型类实例不会携带用于创建它的类型实参的信息,也因此无法检查它们。
虽然无法检查某个值是否是包含某种具体类型,但可以判定是否是包含泛型类型的值,只不过必须用星号投影。
星号投影目前可以被认为拥有未知类型实例的泛型类型。
示例:
//会报异常:Cannotcheck for instance of erased type
if(value is List<String>){ .... }
//正常
if(value is List<*>){ .... }
特别地,as和as?转换中可以使用一般的泛型类型,只是只能确保基础类型正确,而不能保证类型实参正确。
示例:
//声明
fun printSum(c:Collection<T>){
val intList = c as? List<Int> ?: throw IllegalArgumentException("List is expected")
println(intList.sum())
}
//调用
//异常List is expected
printSum(setOf(1,2,3))
//异常String cannot be cast to number,也就是List转换成功,只是使用到元素时才有问题
printSum(listOf("a","b","c"))
//正常
printSum(listOf(1,2,3))
注意,当类型信息确认时,Kotlin编译器是可以进行is检查的。如
fun printSum(c:Collection<Int>){
if(c is List<Int>){
println(c.sum())
}
}
9.2.2 声明带实化类型参数的函数
上面说过,一般Kotlin泛型在运行时也会被擦除,那么也就无法判定相应实例创建时用的是什么类型实参。
Kotlin中有一种例外可以避免这个限制:带实化类型参数的内联函数。
前面第8章讨论过inline函数,编译器会把实现内联函数的字节码插入每一次调用发生的地方。每次调用带实化类型参数的函数时,编译器都知道这次特定调用中用作类型实参的确切类型。因此,编译器可以生成引用作为类型实参的具体类的字节码。这样也就不会被运行时的类型擦除影响。
标准库函数filterIsInstance就是用的内联函数和实化类型参数,有兴趣的可以去查看源码。
这里给一个简单示例:
//声明,reified用来标明实化类型参数
inline fun <reified T> isA(value:Any) = value is T
//调用
println(isA<String>("abc"))
println(isA<String>(123))
>>>>>
true
false
这里有几个注意点:
- 带reified的inline函数不能在Java代码中调用,普通的内联函数可以被调用而不能被内联。
- 需要额外处理,来吧类型实参的值替换到字节码中
- 一个内联函数可以有多个实化类型参数,也可以同时拥有非实化类型参数和实化类型参数。
- 由于内联函数会被整体复制到调用处,为了保证性能,最好把不依赖实化类型参数的代码抽取到单独的非内联函数中。
9.2.3 使用实化类型参数代替类引用
实化类型参数的常见使用场景是为接受java.lang.Class类型参数的API构建适配器。
示例:
//声明
inline fun <reified T:Activity> Context.startActivity(){
//T::class.java是获取java.lang.Class对应的Kotlin类,后面章节会讨论反射相关问题
val intent = Intent(this,T::class.java)
startActivity(intent)
}
//调用
startActivity<SomeActivity>()
9.2.4 实化类型参数的限制
1.使用实化类型参数的场景:
- 用在类型检查和类型转换中,如is、!is、as、as?等
- 使用Kotlin反射API,第10章会介绍,::class
- 获取相应的::class.java
- 作为调用其他函数的类型实参
2.不能使用的场景:
- 创建指定为类型参数的类的实例
- 调用类型参数类的伴生对象的方法
- 调用带实化类型参数函数的时候使用非实化类型形参作为类型实参
- 把类、属性或者非内联函数的类型参数标记成reified
- 如果使用实化类型参数函数,又不想可能有的lambda被内联,可以用noinline修饰
总的来说,关键点在于给予编译器的信息是否足够,若足够则可以进行类型判断转换,否则不能。
9.3 变型:泛型和子类型化
变型的概念描述了拥有相同基础类型和不同类型实参的的类型之前是如何关联的。
9.3.1 为什么存在变型:给函数传递实参
把一个字符串列表传给期望Any对象列表的函数是否安全?
如果函数添加或者替换了列表中的元素就是不安全的,因为这样会产生类型不一致的可能性。而如果列表中的元素只是可读的,那么它就是安全的。
在Kotlin中,如果函数接受的是只读列表,可以传递更具体的元素类型的列表,而如果列表是可变的,就不能这样做。
本节后面的内容会把同样的问题推广到任何泛型类,而不仅仅是list。
9.3.2 类、类型和子类型
1.类型:变量的类型规定了变量可能的值,但不完全等同于类。
- 非泛型类,类的名称可以直接当做类型使用,但同样的类名称可以用来声明可空类型。
- 泛型类则更为复杂,一个合法类型就是用一个作为类型实参的具体类型替换泛型类的类型形参,如
List<Any>
,可以在合适条件下用List<Int>
、List<Number>
等代替。
2.子类型:任何时候如果需要的是类型A的值,都能够使用类型B的值替换,那么类型B就称为类型A的子类型。
- 这个定义表明任何类型都可以被认为是它自己的子类型。
3.超类型:子类型的反义词,如果A是B的子类型,那么B就是A的超类型。
4.重要性:
- 编译器在每一次给变量赋值或者给函数传递实参的时候都要做这项检查,只有可被替换,实际值是声明值的子类型,程序才可以正常编译。
- 子类和子类型本质是不一样的,只是简单情况时,两者表达意思相近。
- 相近:Int类是Number的子类,Int类型是Number类型的子类型。
- 不同:非空类型A是可空类型A?的子类型,但A并不是A?的子类。
- 如上节所述,
MutableList<Int>
不是MutableList<Any>
的子类型,List<Int>
是List的子类型。
5.不变型:一个泛型类,例如MutableList,如果对于任意两种类型C和D,MutableList<C>
既不是MutableList<D>
的子类型也不是它的超类型,它就被称为在该类型参数是不变型的。
9.3.3 协变:保留子类型化关系
1.in位置和out位置
在类成员的声明中,类型参数的使用可以分为in位置和out位置。
- in位置:如果类型参数T被用作函数的参数类型,它就在in位置,这样的函数也称为消费类型为T的值。
- out位置:如果类型参数T被用作函数的返回类型,它就在out位置,这样的函数也称为生产类型为T的值。
2.协变
如果一个协变类是一个泛型类,如Producer<T>
,那么对于两个任意类型C和D,C是D的子类型的话,Producer<C>
也是Producer<D>
的子类型。也称之为子类型化被保留了。
Kotlin中要声明类在某个类型参数上是协变的,在该类型参数名称前加上out关键字即可。
类型参数T上的关键字out有两层含义:
- 子类型化会被保留。
- T只能用在out位置,不能用在in位置。
示例:
interface Producer<out T>{
fun produce():T
}
3.注意点
3.1.类型形参不光可以直接当做参数类型或者返回类型使用,还可以当做另一个类型的类型实参。这时候在什么位置要看具体情况。
示例:
interface List<out T>:Collection<T>{
//这里的T在out位置
fun subList(fromIndex:Int,toIndex:Int):List<T>
}
3.2.不能把MutableList<T>
在它的类型参数上声明成协变的,因为它可以接受T,也可以返回T,也就是T出现在了in和out位置上。
3.3.构造方法的参数既不在in位置,也不在out位置。即使类型参数声明成了out,仍然可以在构造方法参数的声明中使用它。
这是因为如果把类的实例当成一个更泛化的类型的实例使用,变型会防止该实例被误用:不能调用存在潜在危险的方法。构造方法不是那种在实例创建以后还能调用的方法,因此它不会有潜在的危险。
特别的,如果在构造方法的参数上使用了关键字val和var,也就同时声明了getter和setter,对val修饰的只读属性来说,类型参数就用在了out位置,而var修饰的可变属性则是in位置和out位置。这时要注意类型参数前面是否可以用out或in,是否匹配前面的规则。
3.4.位置规则只覆盖了类外部可见的API,私有方法的参数既不在in位置也不在out位置。
//如果没有private修饰时,是不能用out修饰T的
class Herd<out T:Animal>(private var leadAnimal:T,vararg animals:T){ ... }
9.3.4 逆变:反转子类型化关系
逆变:一个在类型参数上逆变的类是这样的一个泛型类,例如Consumer<T>
,对于两个任意类型C和D,如果D是C的子类型,但Consumer<C>
是Consumer<D>
的子类型,也称之为子类型化被反转了。
Kotlin中要声明类在某个类型参数上是逆变的,在该类型参数名称前加上in关键字即可。
in关键字的意思是对应类型的值是传递进来给这个类的方法的,并且被这些方法消费。
类型参数T上的关键字in有两层含义:
- 子类型化会被反转。
- T只能用在in位置,不能用在out位置。
一个类可以在一个类型参数上协变,同时在另外一个类型参数上逆变。
Function接口就是一个经典例子。
interface Fuction1<in P,out R>{
operator fun invoke(p:P):R
}
Kotlin的表示法§ -> R 是表达Function<P,R>
的另一种更具可读性的形式。
9.3.5 使用点类型:在类型出现的地方指定变型
声明点变型:在类声明的时候指定变型修饰符。这些修饰符会应用到所有类被使用的地方。
使用点变型:每一次使用带类型参数的类型的时候,还可以指定这个类型参数是否可以用它的子类型或者超类型替换。
Java支持使用点变型,如(? extends) 和 (? super),Kotlin则两者都支持。
本小节主要讨论Kotlin使用点变型的情况。
当调用了那些类型参数只出现在out位置或者in位置的方法时就可以在函数定义中给特定用途的类型参数加上变型修饰符起到限制和规范传入参数的类型。
这也称之为使用类型投影,因为这是类型参数受限制,只能在out修饰后用在out位置,或者in修饰后用在in位置。
用一个示例来展示:
fun <T> copyData(source:MutableList<T>,destination:MutableList<T>){
for(item in source){
destination.add(item)
}
}
//引入第二个泛型参数来让函数支持不同类型列表
fun <T:R,R> copyData(source:MutableList<T>,destination:MutableList<R>){
for(item in source){
destination.add(item)
}
}
//用变型修饰符来改进
fun <T> copyData(source:MutableList<out T>,destination:MutableList<T>){
for(item in source){
destination.add(item)
}
}
几个注意点:
- 可以为类型声明中类型参数的任意用法指定变型修饰符,如形参类型、局部变量类型、函数返回类型等。
- 如果类型参数已经有out变型,获取它的out投影没有任何意义。例如
List<T>
已经是不可变类型,只能放在out位置,那么List<out T>
就和List<T>
是一个意思。Kotlin编译器会对此发出警告,表明这样的投影多余。 - 上面说的out的用法和注意事项,对in修饰符也是同样存在的。
- Kotlin中的
<out T>
对应于Java中的<? extends T>
,<in T>
对应于Java中的<? super T>
9.3.6 星号投影:使用*代替类型参数
星号投影用来表明不知道关于泛型实参的任何信息。
例如一个包含未知类型的元素列表用这种语法表示为List<*>
。
这里有几个注意点:
<*>
和<Any?>
是不一样的,前者是说包含的是任意类型的元素,后者是说包含的是特定类型的元素。- 不能向
<*>
代表的类型去写入任何值,但可以读取。因为写入的话可能不匹配特定类型,读取则可以按Any?来处理。也就是<*>
一般可以被投影成<out Any?>
Consumer<in T>
这样的逆变参数来说,星号投影等价于<in Nothing>
,也就是无意义的。- Kotlin的
MyType<*>
对应于Java的MyType<?>
总结来说,星号投影语法简洁,但只能用在对泛型类型实参的确切值不感兴趣的地方:只是使用生产值的方法,而且不关心值的类型。
示例:
//声明
fun printFirst(list:List<*>){
if(list.isNotEmpty()){
println(list.first())
}
}
//调用
printFirst(listOf("aa","bb"))
>>>>>>
aa
9.4 小结
本章节讲解泛型的相关知识,一般的泛型使用还是很简单的,难点在于in和out修饰符的使用,以及协变逆变的概念。
这里其实没必要再特意总结什么了,逆变协变,in和out,都是Kotlin关于自己类型系统的进一步的深入,不明白的要多看几遍,看懂了就会体会到Kotlin对于类型系统的重视以及设计的思考。
原文地址:https://blog.csdn.net/crazyf2015/article/details/143478517
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!