Kotlin语法糖--类和对象(下)之泛型
现在到了本节最重要的部分了,我们将要对Kotlin的泛型进行学习。泛型部分内容相对之前的学习稍有复杂,所以单独开篇进行讲解
与Java一样,Kotlin中的类也可以使用类型参数
class A<T>(val t: T) {
fun printlnValue() {
println("$t")
}
fun getValue() : T {
return t
}
fun <T> printlnValue2(t1: T) {
println("$t1")
}
}
如果要完整的使用这个类的话,应该这样初始化
val a:A<Int> = A(12)
但是,如果类型参数可以推断出来的话,这个类型参数就可以省略掉了
val a:A = A(12)
-
型变
先来看一段代码
val list: MutableList<Number> = MutableList<Number>(5) {
0
}
list.add(1)
val list2: MutableList<Int> = MutableList<Int>(5) {
0
}
list.addAll(list2)
list=list2 // 编译出错
我们声明并初始化了Number的集合,又声明并初始化了Int的集合。我们知道Int是Number的一个子类,但是现在变成了他们所属类型的集合,而且我发现一个很奇怪的地方,MutableList<Number>与MutableList<Int>不能像子类覆盖父类一样直接赋值,说明他们之间没有任何关系,但是为什么MutableList<Number>可以直接通过addAll方法将MutableList<Int>添加进来呢?这两个集合之间到底是什么关系?是子类,或者是父类,还是两者真的没有任何关系呢?这就取决于泛型的类型--型变。我们就带着问题来找答案吧
型变,分变和不变;变又分协变与逆变
不变,就是声明的是什么类型,使用或传递的就要是什么类型,就像上文添加Number类型及其子类一样。MutableList<Number>与MutableList<Int>在正常情况确实没有任何关系,所以讲道理是不能添加进来的,但是我们看看源码
override fun addAll(elements: Collection<E>): Boolean
public interface Collection<out E> : Iterable<E>
在addAll的时候,实际上是将E泛型类型的Collection传递进去,这边没毛病,毛病在于,你看到了那个out了吗?
先简单回顾一下Java泛型中的有界类型
<? extends T>声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类
<? super T>声明了类型的下界,表示参数化的类型可能是所指定的类型,或者是此类型的父类型,直至Object
通过代码细细体会
通配符类型使用
简而言之,extends通配符使得整个类型是协变的:只能从集合中获取项目。反过来如果只能向集合中放入项目,那就是super通配符的作用了,这个称为逆变
Kotlin中用生产者和消费者概念来描述这一功能。生产者就是只能用来读取的,也就是extends;而消费者就是只能用来写入的,也就是super,然后起了一个好记的名字--PECS(Producer-Extends, Consumer-Super)。
Kotlin中的协变与逆变分别以out和in2个型变注解来表示。以Number与Int为例总结一下型变中父类与子类之间的关系:
不变性(默认情况): X<Number>和X<Int>没有任何关系
协变性(out): X<Int>是X<Number>的子类型
逆变性(in):X<Number>是X<Int>的子类型
-
声明处型变
来看看Java中的一个问题,本来有这么一个泛型接口
interface Impl<T> {}
然后我准备把一个String类型的集合赋值到一个Object类型的集合中,表面上看好像没什么问题,然而事实是
报错信息
尽管我们认为这没什么问题,子类放到父类中,但是编译期不这么认为,他觉得Impl<Object>并不是Impl<String>的父类。没关系那我们就用通配符类型来处理吧
Impl<? extends Object> objectImpl=new Impl<String>() {};
这样就没啥问题,这个就是Java的使用处型变。那我们来看看同样的问题在Kotlin中怎么处理?同样来一个接口
interface B<out T>
在kotlin中一样报错,这个我们就不管了
image.png
我们可以依然像Java一样在使用处进行型变
val b1: B<out Any> = object : B<String> {}
重点来了,怎样在Kotlin中进行声明处型变
interface B<out T>
val b1: B<Any> = object : B<String> {}
我们就改造了一下接口,使用接口的代码并没有做任何改动工作
由于out作用在类型参数声明处,所以我们称此为声明处型变
-
使用处型变
刚才已经说过了使用处型变,这个与Java是一样的。但是有一点我们要补充说明一下,参数注解 out 用于表示该泛型只能用于输出,例如作为返回值,参数注解 in 用于表示该泛型只能用于输入,例如作为参数。这个其实很好理解,out的时候连类型都不知道,还怎么用呢?但是出来的时候我是知道它至少是父类,大不了返回父类用呗。同理in的时候我知道可以添加哪些类型,但是作为返回值出来的时候我又不知道会是什么类型了。
class H<in T> {
fun printlnValue(t: T) {}
}
abstract class I<out T> {
abstract fun printlnValue() : T
}
class J : I<Number>() {
override fun printlnValue(): Number {
return 1
}
}
在这种情况下,如果我们使用声明处型变,那就不能同时使用泛型格式的输入与输出了。一个很好的例子:Array,Array可以添加某一类型值,同时又可以输出该类型值,来看看Array的源码
public class Array<T> {
public inline constructor(size: Int, init: (Int) -> T)
public operator fun get(index: Int): T
public operator fun set(index: Int, value: T): Unit
public val size: Int
public operator fun iterator(): Iterator<T>
}
来看看用法示例
fun copy(from: Array<out Any>, to: Array<Any>) {
for (index in from.indices) {
to[index]=from[index]
}
}
val from: Array<Int> = arrayOf(1, 2, 4)
val to: Array<Any> = Array(3) {}
copy(from, to)
从from中取出至少为Any类型的值,这样就不会造成类型转换失败
再来看看in的使用示例
fun copy2(from: Array<Int>, to: Array<in Int>) {
for (index in from.indices) {
to[index]=from[index]
}
}
写入to中的至少为Int及其子类,同样也不会造成类型转换失败
为了不让你晕掉,有一个特例我放在最后进行说明:实际上有些情况下,我们不得已需要在协变的情况下使用泛型参数类型作为方法参数的类型
class G<out T> {
fun printlnValue(t: @UnsafeVariance T) {}
}
为了让编译器放过一马,我们就可以用@UnsafeVariance来告诉编译器:“我知道我在干啥,保证不会出错,你不用担心”。
-
星投影
有时候你不知道类型参数到底是什么,又想以安全的方式使用它,这时候星投影就是最安全的使用方式了。该泛型类型的每个具体实例将是该投影的子类型
星投影的语法如下:
(1) 对于Foo<out T>,那么Foo<>与之等价
(2) 对于Foo<in T>,那么Foo<>相当于Foo<in Nothing>,Nothing是一个类并且没有实例,这意味着无法进行写入
(3) 对于Foo<T>,读值的时候与(1)的情况相同,写值的时候与(2)的情况相同
简而言之,当不知道in的类型时,可以安全地防止写入;当不知道out的类型时,用法与原意一样
我们来看看例子
fun star1(c: C<String, *>) {
c.b("123")
c.a()
}
fun star2(c: C<*, Int>) {
c.a()
}
fun star3(c: C<*, *>) {
c.a()
}
-
泛型函数
不仅类有类型参数,函数也可以有,类型参数要放在函数名称前
fun <T> getValue(t: T) {}
fun <T> T.baseToString() : String {
return this.toString()
}
注意这里的泛型不能定义为in或out
-
泛型约束
泛型有一定局限性。例如一个函数的功能是取出两个值中的最小值。但是两个泛型参数无法完成比较(因为可能是任何类型)。解决方案就是限制泛型的类型,即泛型的有界性。
最常见的约束类型是extends关键字对应的上界
abstract class D {
abstract fun printlnD()
}
interface E {
fun printlnE()
}
fun <T: D> getElementOne(t: T) {
t.printlnD()
}
冒号后面是指定类型的上界,即只有D的子类型可以替代T。如果没有指定上界,那么默认的上界是Any?。尖括号只能指定一个上界,如果有多个的话需要一个where子句,例如
fun <T> getElementTwo(t: T)
where T: D,
T: E {
t.printlnD()
t.printlnE()
}
这样只有同时满足实现E接口以及继承D类的对象才能作为getElementTwo方法的入参
class F : D(), E {
override fun printlnD() {
}
override fun printlnE() {
}
}
getElementTwo(F())