泛型总是很难融会贯通的一块,有时间好好学习整理一下。
一、泛型基础
将共通的部分抽象化,复用代码,形成模板。
举一个简单的动物进食的例子。
接口
interface Animal {
fun eat()
}
实现类
class Rabbit : Animal {
override fun eat() {
print("吃草")
}
}
class Tiger : Animal {
override fun eat() {
print("吃肉")
}
}
假如我们需要分析不同动物的进食行为,我们没有必要为每个动物写一个分析进食的方法,使用泛型如下:
class AnalyzeAnimal<A : Animal> { //A : Animal泛型上界:必须是Animal或者Animal的子类
fun analyzeEat(animal: A) {
animal.eat()
}
}
//调用
AnalyzeAnimal<Rabbit>().analyzeEat(Rabbit())
上面的例子就是一个比较基础的简单泛型。
二、型变(Variance)
型变是什么?
简单来说,它就是为了解决泛型的不变性问题。事实上,型变讨论的是:在已知 Rabbit 是
Animal 的子类的情况下,MutableList<Rabbit>与MutableList<Animal>之间是什么关系。
在正常情况下,编译器会认为它们两者是没有任何关系的。换句话,也就是说,泛型是不变
的。Kotlin 编译器会这样处理的原因也很简单,这里我们可以先来假设一下:如果编译器不阻
止我们用MutableList<Rabbit>来替代MutableList<Animal>,代码会出什么问题呢?
fun foo(list: MutableList<Animal>) {
//存子类对象
list.add(Rabbit())
list.add(Tiger())
//只能取出父类对象,因为是父类对象的集合
val animal: Animal = list[0] //通过
//下面二行报错
val rabbit: Rabbit = list[0] //报错
val tiger: Tiger = list[1] //报错
}
fun foo2(list: MutableList<Rabbit>) {
//报错,不能存父类对象
list.add(Animal())
}
所以,在默认情况下,编译器会认为MutableList<Rabbit>与MutableList<Animal>之间不
存在任何继承关系,它们也无法互相替代。这就是泛型的不变性。
三、逆变(Contravariant)
接第一段的泛型基础例子,假如现在我们需要输出兔子的分析报告,方法如下:
private fun report(analyzeAnimal: AnalyzeAnimal<Rabbit>) {
analyzeAnimal.analyzeEat(Rabbit())
}
当我们调用方法时:
report(AnalyzeAnimal<Animal>()) //代码报错
report(AnalyzeAnimal<Rabbit>()) //编译通过
所有的具体实现都在report方法中,只是限制了泛型的类型为Rabbit,结果传入父类Animal代码报错,那有没有方法解决这个问题呢?当然有。
方法一:使用处逆变。
private fun report(analyzeAnimal: AnalyzeAnimal<in Rabbit>) { //变化在这里,添加了in
analyzeAnimal.analyzeEat(Rabbit())
}
方法二:声明处逆变。
class AnalyzeAnimal<in A : Animal> { //变化在这里,添加了in
fun analyzeEat(animal: A) {
animal.eat()
}
}
这样修改之后,我们就可以在report方法中使用AnalyzeAnimal<Animal>来替代AnalyzeAnimal<Rabbit>,也就是说AnalyzeAnimal<Animal>是AnalyzeAnimal<Rabbit>的子类。这种父子关系颠倒的现象,我们就叫做泛型的逆变。上面这两种修改方式,就分别叫做使用处逆变和声明处逆变。
四、协变(Covariant)
除了父子关系颠倒的现象,泛型当中还存在一种父子关系一致的现象,也就是泛型的协变。
//食物
open class Food {}
//川菜
class ChuanFood : Food() {}
//湘菜
class XiangFood : Food() {}
现在有个饭馆,什么风格的菜都能做:
//食物
open class Food {}
//川菜
class ChuanFood : Food() {}
//湘菜
class XiangFood : Food() {}
//饭店
class Restaurant<T : Food> {
//做菜
fun cook(): T {...}
}
//下单做菜
fun orderFood(restaurant: Restaurant<Food>): Food {
return restaurant.cook()
}
调用
orderFood(Restaurant<Food>())
orderFood(Restaurant<ChuanFood>()) //代码报错
假如我们传入ChuanFood()就会报错,但是ChuanFood()是Food()的子类,如果我们还想让饭店提供川菜怎么办呢? 有二种办法。
方法一:使用处协变。
//下单做菜
fun orderFood(restaurant: Restaurant<out Food>): Food { //添加了out
return restaurant.cook()
}
方法二:声明处协变。
//饭店
class Restaurant<out T : Food> { //添加了out
//做菜
fun cook(): T {...}
}
五、星投影(Star-Projections)
所谓的星投影,其实就是用“星号”作为泛型的实参。
那么,什么情况下,我们需要用星号作为泛型实参呢?答案其实也很简单,当我们不关心实参到底是什么的时候。
//饭店
class Restaurant<out T : Food> { //添加了out,限定是Food或其子类
//做菜
fun cook(): T {...}
}
//下单做菜
fun orderFood(restaurant: Restaurant<*>): Food { //*不关心是Food还是其子类
return restaurant.cook()
}
//调用
orderFood(Restaurant<Food>())
orderFood(Restaurant<ChuanFood>())
六、到底什么时候用逆变,什么时候用协变?
Kotlin 的官方文档,有这样一句话:Consumer in, Producer out !,大概意思就是:消费者 in,生产者 out,通俗的话讲就是:需要使用传入数据T的地方(消费者Consumer)用in,生产数据T给调用处调用(生产者Producer)的用out。用前面的例子来说明:
class AnalyzeAnimal<in A : Animal> { //变化在这里,添加了in
fun analyzeEat(animal: A) {
animal.eat()
}
}
上面的泛型A是以函数的参数的形式,被传入到函数的里面,这往往是一种写入行为,这时候,我们用关键字in。
//饭店
class Restaurant<out T : Food> { //添加了out
//做菜
fun cook(): T {...}
}
上面的泛型T最终以返回值的形式,被传出到函数的外面,这往往是一种读取行为,这时候我们用关键字out。
总结就是:传入in,传出out,或者泛型作为参数的时候用in,泛型作为返回值的时候用out。
kotlin源码中的逆变与协变,有利于更好的理解:
1、逆变
package kotlin
/**
* Classes which inherit from this interface have a defined total ordering between their instances.
*/
public interface Comparable<in T> {
/**
* Compares this object with the specified object for order. Returns zero if this object is equal
* to the specified [other] object, a negative number if it's less than [other], or a positive number
* if it's greater than [other].
*/
public operator fun compareTo(other: T): Int //T为传参
}
比较的对象可以是Int,Long,Boolean等,与传入的泛型T相关,这时候使用逆变比较合适。
2、协变
public interface Iterator<out T> {
/**
* Returns the next element in the iteration.
*/
public operator fun next(): T //T为返回值
/**
* Returns `true` if the iteration has more elements.
*/
public operator fun hasNext(): Boolean
}
迭代让子类和父类输出保持一致性,T为返回值,这时候用协变比较合适。
七、@UnsafeVariance
看一段kotlin官方的源码,如下:
public interface List<out E> : Collection<E> {
override val size: Int
override fun isEmpty(): Boolean
override fun contains(element: @UnsafeVariance E): Boolean
override fun iterator(): Iterator<E>
override fun containsAll(elements: Collection<@UnsafeVariance E>): Boolean
public operator fun get(index: Int): E
public fun indexOf(element: @UnsafeVariance E): Int
public fun lastIndexOf(element: @UnsafeVariance E): Int
public fun listIterator(): ListIterator<E>
public fun listIterator(index: Int): ListIterator<E>
public fun subList(fromIndex: Int, toIndex: Int): List<E>
}
泛型E在get(index: Int): E中是作为生成者,但是在contains(element: @UnsafeVariance E): Boolean中又是作为消费者,为了解决泛型的这种冲突,需要额外的添加@UnsafeVariance注解。
八、泛型中的类型擦除与reified
1、什么是类型擦除
看下面的例子
val intList = mutableListOf<Int>()
val strList = mutableListOf<String>()
Log.d("TAG", "${intList == strList}") //true
虽然是二个不同泛型类型的集合,但是当我们比较二个对象的时候,得到的结果就是true。正是由于类型被擦除,所以就导致一些相关问题,比如不安全的类型转换,模版方法的增多等。
2、如何解决类型擦除
Kotlin中存在名为 reified 的关键字,它可以被作用于函数上, 以此做到类型擦除后的再生,便于开发者优雅的使用泛型以及获取方法的泛型类型。但需要注意的是,reified 关键字必须和 inline 关键字一起使用。比如下面的例子:
inline fun <reified T : AppCompatActivity> Context.startActivityKtx() {
startActivity(Intent(this, T::class.java))
}
//调用
startActivityKtx<TwoActivity>()
反编译后看使用reified关键字是如何具象化泛型对象的:
public final class StringDelegateKt {
public static final void startActivityKtx(Context $this$startActivityKtx) {
//...
$this$startActivityKtx.startActivity(new Intent($this$startActivityKtx, AppCompatActivity.class));
}
}
//调用
this.startActivity(new Intent(this, TwoActivity.class));
使用reified泛型T被具象化为AppCompatActivity.class或其子类,使用inline关键字关键性的代码被内联到了调用处,所以也没有性能上的问题。
模仿上面的例子写一个我们自己的函数:
inline fun <reified AN : Animal> AppCompatActivity.animals() {
analyze(AN::class.java)
}
fun <AN : Animal> analyze(java: Class<AN>) {
java.newInstance().eat()
}
//调用
animals<Rabbit>()
注意不要这样写,因为具象化后会调用父类的方法:
inline fun <reified AN : Animal> AppCompatActivity.animals() {
AN::class.java.newInstance().eat()
}
//调用
animals<Rabbit>()
烦编译调用处的代码
((Animal)Rabbit.class.newInstance()).eat();
Rabbit被强转成了父类Animal。
九、泛型实例化
泛型实例化在上面第八点有介绍,借助关键字reified和inline可以实例化泛型T对象,但上面的代码比较简单,看具体是如何实例化的。
1、无参构造实例化
inline fun <reified T : Any> new(): T =
T::class.java
.getDeclaredConstructor() //获取所有构造器
.apply { isAccessible = true } //关闭安全检查达到提升反射速度的目的
.newInstance()
2、有参构造实例化
inline fun <reified T : Any> new2(vararg params: Any): T =
T::class.java
.getDeclaredConstructor(* params.map { it::class.java }.toTypedArray()) //获取所有构造器 //Array转可变参数vararg前面需加*号
.apply { isAccessible = true } //关闭安全检查达到提升反射速度的目的
.newInstance(params)