10.6 泛型的高级特性

10.6.1 对泛型进行实化

泛型实化是Kotlin中新引入的概念。

首先我们需要了解一点,所有基于JVM的语言,如Java、Kotlin,它们的泛型功能都是通过类型擦除来实现的。

这种机制使得我们不能使用a is T或者T::class.java这样的语法,因为T的实际类型在运行时已经被擦除了。

然而不同的是,Kotlin提供了内联函数的概念,内联函数中的代码在编译的时候会自动被替换到调用它的地方,这样的话就不存在泛型擦除的问题了,因为代码在编译之后会直接使用实际的类型来替代内联函数中的泛型声明。

这也是为什么只有内联函数才能在泛型声明的地方加上reified关键字来进行实化。

示例代码如下:

inline fun <reified T> getGenericType() = T::class.java

这里我们对T泛型进行了实化,这样我们就能在getGenericType()这个方法里直接调用T::class.java等方式获取该泛型的实际类型。

我们写个测试方法来对这个内联函数进行调用测试。

@Test
fun test01() {
    val result1 = getGenericType<String>()
    val result2 = getGenericType<Int>()
    println("result1 is $result1")
    println("result2 is $result2")
}

查看输出。

result1 is class java.lang.String
result2 is class java.lang.Integer

这样一来,我们就对泛型实化的概念有一定了解了。

10.6.2 泛型实化的应用

本节举例了一个泛型实化的应用,来加深印象。

首先定义了一个内联函数startActivity()

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

这个函数的泛型加上了reified的关键字,说明该泛型已经被实化了。

那么在方法中我们直接在创建Intent对象的时候,第二个参数直接获取T泛型对应的类型即可。

在调用的时候直接这么写:

@Test
fun test02() {
    startActivity<TestActivity>(context = context)
}

看,是不是方便了很多。

不过我们通常在页面跳转的时候需要给Intent赋值一些参数,这时候我们就可以借助之前几章高阶函数的知识,来对代码进行优化。

inline fun <reified T> startActivity(context: Context, block: Intent.() -> Unit) {
    val intent = Intent(context, T::class.java)
    intent.block()
    context.startActivity(intent)
}

如此一来,我们就可以很轻松的给intent进行赋值参数了。

@Test
fun test02() {
    startActivity<TestActivity>(context){
        putExtra("param1", "data")
        putExtra("param2", 123)
    }
}

通过这个应用,相信大家已经能领会到泛型实化的应用场景了。

10.6.3 泛型的协变

协变和逆变内容很多同学都比较薄弱,这源于对java中的泛型就缺乏深入的掌握。
因此笔者建议先看一下凯哥的视频——Kotlin 的泛型,再继续看本文章这两节的内容。

在学习协变和逆变之前,先了解一个约定。

一个泛型类或者泛型接口中的方法,它的参数列表是接收数据的地方,因此可以称它为in位置,而它返回值是输出数据的地方,可以称它为out位置。

如图所示:

Android kotlin 泛型 in out kotlin 泛型解决擦除_java


有了这个前提,我们来定义三个类:

open class Person(val name: String, val age: Int)

public class Student(name: String, age: Int): Person(name, age)

public class Teacher(name: String, age: Int): Person(name, age)

我们知道子类对象是可以赋值给父类的引用,这是完全没问题的。

但是思考一个问题:那么子类类型的List能赋值给父类类型的List吗?

也就是说List<Person> = List<Student>,可以这么定义吗?

这么定义是不合法的,存在安全隐患,举例说明:

class SimpleData<T> {
    private var data: T? = null

    fun set(t: T?) {
        data = t
    }

    fun get(): T? {
        return data
    }
}

上面定义了一个泛型类SimpleData

再定义一个handleSimpleData函数,他的参数是SimpleData<Person>,如下。

@Test
fun test03() {
    val student = Student("Tom", 19)
    var data = SimpleData<Student>()
    data.set(student)
    handleSimpleData(data = data) // 实际上这里会报错,我们假设它能编译通过
    val get = data.get()
}

fun handleSimpleData(data: SimpleData<Person>) {
    val teacher = Teacher("Jack", 19)
    data.set(teacher)
}

我们假设调用handleSimpleData()方法的时候可以传入SimpleData<Student>,也就是handleSimpleData(data = data)这行代码能正常执行。

这时候如果在handleSimpleData()方法中定义一个Teacher对象,然后调用data.set(teacher)把这个Teacher对象赋值给SimpleData中的data属性上。

这时候就会出现类型转换错误,因为handleSimpleData的形参实际上是一个SimpleData<Student>(),但是data.set(teacher)的却是一个Teacher类的对象。

泛型协变:假如定义了一个MyClass<T>的泛型类,其中AB的子类型,同时MyClass<A>又是MyClass<B>的子类型,那么我们就可以称MyClassT这个泛型上是协变的。

如何让MyClass<A>成为MyClass<B>的子类呢,就是在泛型T的声明前加上out关键字(相当于Java中的? extend关键字)。

不过这样也加了一个限制,T只能出现在out位置,也就是其在泛型类型数据上是只读的。

class SimpleData<out T>(val data: T?) {
    fun get(): T? {
        return data
    }
}

由于SimpleData类已经进行了协变声明,那么SimpleData<Student>自然就是SimpleData<Person>的子类,所以就可以安全地向handleMyData()方法中传递参数了。

@Test
fun test03() {
    val student = Student("Tom", 19)
    var data = SimpleData<Student>(student)
    handleSimpleData(data = data)
    val get = data.get()
}

fun handleSimpleData(data: SimpleData<Person>) {
    val get = data.get()
}

10.6.4 泛型的逆变

泛型的逆变与协变刚好相反,我们用一张图来看:

Android kotlin 泛型 in out kotlin 泛型解决擦除_kotlin_02

直接看示例,先定义一个Transformer接口,用来做一些转换操作

interface Transformer<T> {
    fun transformer(t: T): String
}

定义一个方法handleTransformer,接收一个子类的List的参数,然后在测试方法中传入父类的List

@Test
fun test04() {
    val trans = object : Transformer<Person> {
        override fun transformer(t: Person): String {
            return "${t.name} ${t.age}"
        }
    }
    handleTransformer(trans = trans) // 这行会报错
}

fun handleTransformer(trans: Transformer<Student>) {
    var student = Student("Tom", 19)
    var result = trans.transformer(student)
}

这段代码从安全的角度上分析是没有问题的,但是却是不允许的。

如果想让父类的List赋值给子类List的引用,就需要逆变声明。如下:

interface Transformer<in T> {
    fun transformer(t: T): String
}

这样,就能正常编译运行了。

同样,加上了逆变声明后,只能定义在in的位置上,而且可写不可读。

值得一提的是,可以用注解@UnsafeVariance摆脱位置上的限制,但是一旦使用了这个注解,也就告诉了编译器不做这块的安全转换检查,如有异常,会直接报错。

下面是一个安全转换错误的示例:

interface Transformer<in T> {
    fun transformer(name: String, age: Int): @UnsafeVariance T
}
@Test
fun test04() {
    val trans = object : Transformer<Person> {
        override fun transformer(name: String, age: Int): Person {
            return Teacher(name, age)
        }

    }
    handleTransformer(trans = trans)
}

fun handleTransformer(trans: Transformer<Student>) {
    val result = trans.transformer("Tom", 19)
}

最后运行代码的时候,查看会看到控制台中类型转换错误的日志输出。

Android kotlin 泛型 in out kotlin 泛型解决擦除_kotlin_03


书中关于泛型的高级特性就到此为止了。祝好!

Android kotlin 泛型 in out kotlin 泛型解决擦除_泛型_04


附上文中代码地址

《第一行代码》Kotlin讲堂知识整理