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

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

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 可以打破这语法规则,但会带来而外的风险。)

本篇文章就介绍到这。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342