Kotlin-泛型

180 阅读7分钟

泛型总是很难融会贯通的一块,有时间好好学习整理一下。

一、泛型基础

将共通的部分抽象化,复用代码,形成模板。

举一个简单的动物进食的例子。
接口

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)

型变是什么?

简单来说,它就是为了解决泛型的不变性问题。事实上,型变讨论的是:在已知 RabbitAnimal 的子类的情况下,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,LongBoolean等,与传入的泛型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>
}

泛型Eget(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

九、泛型实例化

泛型实例化在上面第八点有介绍,借助关键字reifiedinline可以实例化泛型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)

参考了以下内容

Kotlin | 浅谈 reified 与泛型 那些事

泛型:逆变or协变,傻傻分不清?

个人学习笔记