美文网首页kotlinKotlin学习
Kotlin学习 8 -- 泛型的高级特性

Kotlin学习 8 -- 泛型的高级特性

作者: 开心wonderful | 来源:发表于2020-07-27 12:09 被阅读0次

本篇文章主要介绍以下几个知识点:

SUMMER DAY (图片来源于网络)

1. 对泛型进行实化

在 JDK 1.5中,Java 引入了泛型功能(之前没有),是通过类型擦除机制来实现的,即泛型对类型的约束只在编译时存在,运行时仍按 JDK 1.5之前的机制来运行,JVM 识别不出在代码中指定的泛型类型。

所有基于 JVM 的语言,其泛型功能都是通过类型擦除机制来实现的,包括 Kotlin。这种机制不可使用 a is TT::class.java 的语法,因为 T 的实际类型在运行时已被擦除了。

不过,Kotlin 提供了一个内联函数的概念,内联函数中的代码会在编译时自动被替换到调用它的地方,编译后会直接使用实际的类型替换内联函数中的泛型声明,从而不存在泛型擦除的问题了。

即 Kotlin 中是可以将内联函数中的泛型进行实化的。

泛型实化:函数必须是内联函数,在声明泛型的地方必须加上 reified 关键字,如下:

inline fun <reified T> getGenericType() { 
    // T 就是一个被实化的泛型
}

借助泛型实化就可以实现诸如获取泛型类型的功能:

// 返回当前指定泛型的实际类型
inline fun <reified T> getGenericType() = T::class.java

测试如下:

fun main() {
    val result1 = getGenericType<String>()
    val result2 = getGenericType<Int>()
    println(result1) // java.lang.String
    println(result2) // java.lang.Integer
}

2. 泛型实化的应用

在过去,通常用如下代码启动一个 Activity:

val intent = Intent(context, TestActivity::class.java)
context.startActivity(intent)

下面就借助泛型实化功能来优化这种写法,定义 startActivity() 函数如下:

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

现在,启动 Activity 的代码这样写就可以了:

// Kotlin 能识别出指定泛型的实际类型,并启动相应的 Activity
startActivity<TestActivity>(context)

对于 Intent 传参的问题,可继续添加新的 startActivity() 函数重载如下:

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

这样启动 Activity 并传参就可以这么写了:

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

3. 泛型的协变

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

首先来看个栗子,定义如下3个类:

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

思考如下:

  • 若某个方法接收一个 Person 类型的参数,而传入一个 Student 的实例,是否合适?
    因为 StudentPerson 的子类,从而可以这么传。

  • 若某个方法接收一个 List<Person> 类型的参数,而传入一个 List<Student> 的实例,是否合适?
    在 Java 中是不允许这么做的,因为 List<Student> 不能成为 List<Person> 的子类,否则存在类型转换的安全隐患。(注:Kotlin 中可以,因为 Kotlin 已经默认给许多内置的 API 加上了协变声明,包括各种集合的类与接口)

对于存在类型转换的安全隐患,测试如下:

fun main() {
    val student = Student("Tom", 18)
    val data = SimpleData<Student>()
    data.set(student)
    handleSimpleData(data) // 这里会报错
    val studentData = data.get()
}

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

// 泛型类,内部封装了一个泛型 data 字段
class SimpleData<T> {
    private var data: T? = null
    fun set(t: T?) {
        data = t
    }
    fun get(): T? {
        return data
    }
}

泛型协变:定义一个 MyClass<T> 的泛型类,其中 AB 的子类型,同时 MyClass<A> 又是 MyClass<B> 的子类型,就称 MyClassT 这个泛型上是协变的。

  • 如何让 MyClass<A> 成为 MyClass<B> 的子类型?
    若一个泛型类在其泛型类型的数据上是只读的话,它是没有类型转换的安全隐患的。要实现这点,需要让 MyClass<T> 类中的所有方法都不能接收 T 类型的参数, 即 T 只能出现在 out 位置,而不能出现在 in 位置上。

修改上述代码如下,使其没有类型转换的安全隐患:

fun main() {
    val student = Student("Tom", 18)
    val data = SimpleData<Student>(student) 
    // SimpleData 进行了协变声明,使得 SimpleData<Student> 是 SimpleData<Person> 的子类,
    // 从而可以向 handleMyData() 中传递参数
    handleMyData(data) 
    val studentData = data.get()
}

fun handleMyData(data: SimpleData<Person>) {
    val personData = data.get() // 这里获取到的是 Student 实例
}

// 在泛型 T 的声明前加 out 关键字,
// 即 T 只能出现在在 out 位置上,不能出现在 in 位置上
// 即 SimpleData 在泛型 T 上是协变的
// 构造函数里使用 val 关键字,T 只读(或:用 var 需要在前面加 private) 
class SimpleData<out T>(val data: T?) {
    fun get(): T? {
        return data
    }
}

4. 泛型的逆变

泛型逆变:定义一个 MyClass<T> 的泛型类,其中 AB 的子类型,同时 MyClass<B> 又是 MyClass<A> 的子类型,就称 MyClass 在 T 这个泛型上逆变的。

协变逆变区别如下:


协变与逆变的区别

下面举个栗子来说明下:

fun mian() {
    val trans = object: Transformer<Person> {
        override fun transform(t: Person): String {
            return "${it.name} ${it.age}"
        }
    }
    // 这里会报错,因为 Transformer<Person> 并不是 Transformer<Student> 的子类型
    handleTransformer(trans) 
}

fun handleTransformer(trans: Transformer<Student>) {
    // 创建个 Student 对象,并调用参数的方法
    val student = Student("Tom", 18)
    val result = trans.transform(student)
}

// 接口中声明个接收 T 类型参数的方法
interface Transformer<T> {
    fun transform(t: T): String
}

利用 逆变 就可以处理上面的问题,修改 Transformer 如下:

// 在泛型 T 的声明前加 in 关键字
// 即 T 只能出现在 in 位置,不能出现在 out 位置
// 即 Transformer 在泛型 T 上是逆变的
interface Transformer<in T> {
     fun transform(t: T): String
}

只要加个 in 关键字即可,此时 Transformer<Person> 就成为了 Transformer<Student> 的子类型,编译就能正常运行了。

若逆变时泛型 T 出现在 out 位置,会有类型转换异常。

小结:Kotlin 在提供协变和逆变功能时,已经把潜在的类型转换安全隐患全部考虑进去了,只要按照其语法规则,让泛型在协变时只出现在 out 位置,逆变时只出现在 in 位置,就不会存在类型转换异常的情况。(注:注解 @UnsafeVariance 可以打破这语法规则,但会带来而我的风险。)

本篇文章就介绍到这。

相关文章

网友评论

    本文标题:Kotlin学习 8 -- 泛型的高级特性

    本文链接:https://www.haomeiwen.com/subject/xsgacktx.html