本篇文章主要介绍以下几个知识点:
- 对泛型进行实化
- 泛型实化的应用
- 泛型的协变
- 泛型的逆变
内容参考自第一行代码第3版
1. 对泛型进行实化
在 JDK 1.5中,Java 引入了泛型功能(之前没有),是通过类型擦除机制来实现的,即泛型对类型的约束只在编译时存在,运行时仍按 JDK 1.5之前的机制来运行,JVM 识别不出在代码中指定的泛型类型。
所有基于 JVM 的语言,其泛型功能都是通过类型擦除机制来实现的,包括 Kotlin。这种机制不可使用 a is T
或 T::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
的实例,是否合适?
因为Student
是Person
的子类,从而可以这么传。若某个方法接收一个
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>
的泛型类,其中 A 是 B 的子类型,同时 MyClass<A>
又是 MyClass<B>
的子类型,就称 MyClass
在 T 这个泛型上是协变的。
-
如何让
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>
的泛型类,其中 A 是 B 的子类型,同时 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
可以打破这语法规则,但会带来而外的风险。)
本篇文章就介绍到这。