Kotlin泛型-你可能需要知道这些

4,810 阅读11分钟

本博文主要讲解一些Kotlin泛型的问题,中间会对比穿插Java泛型。

1. 泛型类型参数

1.1 形式

我们使用泛型的形式无非是类、借口、方法几种,我们先看两个例子。

1.2 声明泛型类

和Java一样,我们通过在类名后面添加一对<>,并把类型参数放在<>内来声明泛型类和泛型接口。

一旦声明完成,我们就可以在类和接口内部,像使用其他类型一样使用类型参数了。

我们来看下标准的Java接口List如何使用Kotlin来声明。 Kotlin中List接口的定义。


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>
}

如果你的类继承了泛型类(或者实现了泛型接口),你就得为基础类型的泛型提供一个类型实参,它可以是一个具体的类型或者另外一个类型形参。

class StringList : List<String>{
	override fun get(index:Int):String =...
}

一个简单的泛型类定义:

class Pair<K, V>(key: K, value: V) {
    var key: K = key
    var value = value
}

1.3 泛型方法定义:

fun <T : Any> getClassName(clzObj: T): String {
    return clzObj.javaClass.simpleName
}

泛型T被声明在方法名前面。

泛型T默认为可空类型,并且我们限定了T继承Any,防止clzObj为空。

1.4 和Java泛型的不同之处

(1)和Java不同,Kotlin始终要求类型实参要么被显式的声明,要么能被编译器推到出来。

val readers : MutableList<String> = mutableListOf()
val readers = mutableListOf<String>()

上面这两行代码是等价的。

(2) Kotlin不支持原生类型

Kotlin一开始就有泛型,因此它不支持原生态类型,类型实参必须定义。


2. 类型参数约束

2.1 T上界指定

如果把一个类型指定为泛型类型形参的上界约束,在泛型具体的初始化时,其对应的泛型实参类型就必须为是这个具体类型或者其子类型。

Java中的形式: T extends Number

Kotlin中的形式: T : Number


fun <T : Number> List<T>.sum():T{
	//...
}

一旦指定了类型上界,你就可以在将T当做它的上界类型来使用。

如上面的例子,我们可以将T当作Number类型来使用。

2.2 为一个类型参数指定多个约束

fun <T> ensureTrailingPeriod(seq: T)
        where T : CharSequence, T : Appendable {
    //...
}

上面这个例子,指定了可以作为类型参数实参的类型必须实现了CharSequence和Appendable两个接口。

2.3 让类型形参非空

事实上,没有指定上界的类型形参将会使用Any?这个默认的上界。

看下面的这个例子:

class Processor<T> {
    fun process(value: T) {
        println(value?.hashCode())
    }
}

在process方法中,value是可空的,尽管T没有使用任何的?标记。

如果你想指定任何时候类型形参都是非空的,那么你可以通过指定一个约束来实现。

如果你除了可控性之外没有其他限制,可以使用Any代替默认的Any?作为其上界。

class Processor<T : Any> {
    fun process(value: T) {
        println(value.hashCode())
    }
}

注意:String?不是Any的子类型,它是Any?的子类型;String才是Any的子类型。


3. 运行时泛型

我们知道JVM上的泛型,一般是通过类型擦除来实现的,所以也被成为伪泛型,也就说类型实参在运行时是不保存的。

实际上,我们可以声明一个inline函数,使其类型实参不被擦除,但是这在Java中是不行的。

3.1 类型擦除

和Java一样,Kotlin的泛型在运行时也被擦除了,这意味着实例不会携带用于创建它的类型实参的信息。

例如你创建了一个List并将一堆字符串保存其中,在运行时你只能看到一个List,不能辨别出列表本打算包含的是哪种类型的元素。

看起来类型擦除是不安全的,但在我们编码的时候,编译器是知道类型实参的类型的,确保你只能添加合适类型的元素。

类型擦除的好处就是节省内存。

3.1.1 is

因为类型擦除的原因,所以一般情况下,在is检查中不可能使用类型实参的类型。 对应的Java中,不能在一个确定泛型上执行instanceof操作。

Kotlin不允许使用没有指定类型实参的泛型类型。 那么你可能想知道如何检查一个值是否为列表,而不是Set或者其他对象,可以使用特殊的星号投影语法来做这个检查。

fun <T : Any> process(value: T) {
    if (value is List<String>) {
        // error
    }

    if (value is List<*>) {
        // ok
    }

    var list = listOf<Int>(1, 2, 3)
    if (list is List<Int>) {
        // ok
    }
}

Kotlin的编译器是足够聪明的,如果在编译期已经知道相应的类型信息时,is检查是被允许的。

List<*>相当于Java中的List<?>,拥有某个未知类型实参的泛型类型。 上面的例子中只是检查了value是否为List,而没有得到关于它的元素类型的任何信息。

3.1.2 as/as?

在as和as?转换中,我们仍然可以使用一般的泛型类型。

(1)如果该类有正确的基础类型但类型实参是错误的,转换也不会失败,因为在运行时类型实参是未知的(因为被擦除掉了),但是后面可能会出现ClassCastException。

(2)如果该类型基础类型都不正确的话,as?就会返回一个null值。


fun main(args: Array<String>) {
    var list: List<Int> = listOf(1, 2, 3)
    printSum(list)// 6

    var strList: List<String> = listOf("1", "2", "3")
    printSum(strList)// ClassCastException

    var intSet: Set<Int> = setOf(1, 2, 3)
    printSum(intSet) // IllegalStateException
}

fun printSum(c: Collection<*>) {
    val intList = c as? List<Int> ?:
            throw IllegalStateException("List is expected")
    println(intList.sum())
}

3.2 实化类型参数函数

泛型函数的类型实参,在运行时同样会被擦除。 只有一种特殊的情况:内联函数,内联函数的类型形参能够被实化,意味着在运行时,你可以引用实际的类型实参。


// compile error
fun <T> isA(value: Any) = value is T 

3.2.1 inline

如果使用inline标记函数,编译器会把每一次函数调用都替换为函数实际的代码。

lambda的代码也会被内联,不会创建任何匿名内部类。

inline函数大显身手的另一种场景:他们的类型参数可以被实化。

3.2.2 实化参数函数

inline fun <reified T> isA(value: Any) = value is T

一个实际的例子filterIsInstance:


public inline fun <reified R> Iterable<*>.filterIsInstance(): List<@kotlin.internal.NoInfer R> {
    return filterIsInstanceTo(ArrayList<R>())
}

public inline fun <reified R, C : MutableCollection<in R>> Iterable<*>.filterIsInstanceTo(destination: C): C {
    for (element in this) if (element is R) destination.add(element)
    return destination
}

简化Android的startActivity方法:

inline fun <reified T : Activity> Context.startActivity() {
    val intent = Intent(this,T::class.java)
    startActivity(intent)
}

3.2.2 为什么实化只对inline函数有效

这是什么原理呢?为什么inline函数中可以这样写element is R,而普通的函数不行呢?

正如之前描述的,编译器把实现inline行数的字节码插入到每一次调用发生的地方。 每次你调用带实例化类型参数的inline方法时,编译器都知道这次特定调用中用作类型实参的确切类型,因此编译器可以生成引用实际类型的字节码。

实化类型参数的inline函数不能在Java中调用,普通的内联函数可以在Java中被调用。


4. 变型:泛型和子类型化

4.1 子类和子类型

Int是Number的子类。

Int类型是Int?类型的子类型,他们都对应Int类。

一个非空类型是它的非空版本的子类型,但他们都对应同一个类。

List是一个类,List\List是类型。

明白子类和子类型很重要。

4.2 不变型

一个泛型类,例如MutableList--如果对于任意的两个类型A和B,Mutable既不是Mutable的父类型,也不是它的父类型,那么该泛型类就称为在该类型参数是不变型。

Java中所有的类都是不变型的。

4.3 协变:保留子类型化

一个协变类是一个泛型类(我们以Poducer类为例),如果A是B的子类型,那么Producer也是Producer的子类型;我们说子类型化被保留了。

Kotlin中,要声明在某个类型参数上是可以协变的,在该类型参数的名称前加out关键字即可:

interface Producer<out T> {
    fun produce(): T
}

一个不可变的例子:


open class Animal {
    open fun feed() {
        println("Animal is feeding.")
    }
}

class Herd<T : Animal> {
    val size: Int = 10
    fun get(index: Int): Animal? {
        return null
    }
}

fun feedAllAnimal(animals: Herd<Animal>) {
    for (i in 0 until animals.size) {
        animals.get(i)?.feed()
    }
}

class Cat : Animal() {
    override fun feed() {
        println("Cat is feeding.")
    }
}


fun takeCareOfCats(cats: Herd<Cat>) {
    feedAllAnimal(cats)//comile error
}


很遗憾,在上面的例子中我们不能把猫群当做动物群被照顾。

在没有使用任何通配符的类型参数上,泛型类在类型参数上是不变型的。

那么我们怎样才能让猫群也能被当做动物群被照顾呢,答案很简单,只需要修改Herd类如下即可:

class Herd<out T : Animal> {
    val size: Int = 10
    fun get(index: Int): Animal? {
        return null
    }
}

in & out

在类的成员声明中类型参数的使用位置可以分为in位置和out位置。

如果函数是把T当成返回类型,我们说它在out位置。

如果T用作函数参数类型,它就在in位置。

类的类型参数前使用out的关键字要求所有使用T的方法只能把T放在out位置上,而不能放在in位置。

现在考虑下,我们能否把MutableList中的T声明为协变的?

答案是不能,因为MutableList既可以添加T元素,也可以获取T元素,因此T既出现在了out位置,也出现在了in位置。

在上面的分析中,我们已经看到过List接口的定义,List在Kotlin中是只读的。

public interface List<out E> : Collection<E>{
	//....
}

我们来看下List和MutableList的定义:

public interface MutableList<E> : List<E>, MutableCollection<E>{
	override fun add(element: E): Boolean
	public fun removeAt(index: Int): E
}

方法的定义验证了我们前面的分析,MutableList的类型参数E既不能声明为out E,也不能声明为in E.

构造方法中的参数既不在in位置也不在out位置,即使类型参数声明为out,我们仍然可以在构造方法参数的声明中使用它。

对于类的var属性,我们不能使用out修饰属性的类型参数。 因为属性的setter方法在in位置上使用了类型参数,而getter方法在out位置使用了类型参数。

注意位置规则只覆盖了类外部可见(public\protected\internal)API,私有方法的参数既不在out位置也不在in位置。


// compile error
class Herd<out T : Animal>(var leadAnimal: T) {
    
}

// ok
class Herd<out T : Animal>(private var leadAnimal: T) {
    
}

4.4 翻转子类型化关系

逆变的概念可以看做是协变的镜像:对一个你逆变类来讲,它的子类型化关系与作用类型实参的类子类型化关系是相反的。

逆变类是一个泛型类(我们以Consumer为例),如果B是A的子类型,那么Consumer是Consumer的子类型。 类型A和B交换了位置,所以我们说子类型化被反转了。

逆变对应的关键字是in。

4.5 点变型:在类型出现的地方指定变型

fun <T : R, R> copyTo(source: MutableList<out T>, destination: MutableList<in T>) {
    source.forEach { item -> destination.add(item) }
}

我们说source不是一个普通的MutableList,而是一个投影(受限)的MutableList,只能调用返回类型是泛型参数的那些方法。

Kotlin中的MutableList和Java中的MutableList<? extends T>是一个意思。

Kotlin中的MutableList和Java中的MutableList<? super T>是一个意思。

MutableList<*>的投影为MutableList<out Any?>。

Kotlin中MyType<*>对应Java中的MyType<?>。

5. 总结

Kotlin的泛型和Java的泛型很多都是相同的,只是表现形式不太相同,我们对比学习来加深理解和记忆。

祝各位看官工作愉快。