前言
之前我们讲了 Kotlin 泛型的基本用法,也就是和 Java 中相同的部分。但 Kotlin 为泛型提供了一些特有的进阶功能,能提升代码的简洁性和安全性,我们一起来看看吧。
泛型实化
在了解泛型实化之前,我们先要理解 Java 的泛型擦除机制。
在 JDK 1.5 之前,Java 是没有泛型的,容器(如 ArrayList
)可以存放任意类型的对象。当从容器取出对象后,需要手动进行类型转换。这不仅麻烦,还很危险。要是转成错误的类型,程序就会在运行时抛出类型转换异常 ClassCastException
。
JDK 1.5 中引入的泛型解决了这个问题,给我们带来了编译期的类型安全。
然而这种安全是通过类型擦除 机制实现的。就是泛型的类型约束只在编译时期存在,在运行时,还是按照之前的机制来运行,JVM 识别不出我们为泛型指定的具体类型。例如有 List<String>
和 List<Integer>
,JVM 会认为是同一个 List
。所有基于 JVM 的语言,泛型都是通过类型擦除机制实现的,包括 Kotlin。
这使得我们不能使用像 a is T
、T::class.java
这样的写法,因为泛型 T
的具体类型在运行时已经被擦除了。
但 Kotlin 提供了内联函数。内联函数中的代码会在编译时被"复制"到调用处。编译器在内联时就会知道泛型的实际类型,这就意味着,Kotlin 中可以让泛型进行实化。
那该怎么实现呢?
首先,函数要声明为内联函数;其次在声明泛型时,需要在前面加上 reified
关键字,表示对泛型进行实化。
例如:
kotlin
// reified 关键字告诉编译器,在编译时将 T 的具体类型固化到字节码中
inline fun <reified T> getGenericType(): Class<T> {
return T::class.java
}
上述的泛型 T
就是一个被实化的泛型。那么它能实现什么效果呢?其实从函数名就可以看出:获取泛型的具体类型。
测试一下:
kotlin
fun main() {
val type1 = getGenericType<String>()
val type2 = getGenericType<Int>()
println("type1 is $type1")
println("type2 is class java.lang.Integer: ${type2 == java.lang.Integer::class.java}")
}
运行结果:
kotlin
type1 is class java.lang.String
type2 is class java.lang.Integer: true
泛型实化的应用
在 Android 中,启动一个 Activity 需要传递 Context
和目标 Activity 的 Class
对象:
kotlin
val intent = Intent(context, TestActivity::class.java)
context.startActivity(intent)
借助泛型实化,我们可以简化这种写法。
首先创建 reified.kt
文件,在其中定义一个 Context
的 startActivity()
扩展函数:
kotlin
inline fun <reified T> startActivity(context: Context) {
val intent = Intent(context, T::class.java)
context.startActivity(intent)
}
现在,我们要启动 TestActivity
,只需要这样写:
kotlin
// 在 Activity 或 Fragment 中
startActivity<TestActivity>(context)
Kotlin 能够识别出泛型的具体类型,并且启动对应的 Activity
。
另外,启动 Activity
通常会传递数据,我们可以使用高阶函数来完成。添加一个函数重载:
kotlin
// 定义为 Context 的扩展函数,方便调用
inline fun <reified T : Activity> Context.startActivity(
// 使用带接收者的 Lambda,可以直接在 block 中调用 Intent 的方法
block: Intent.() -> Unit = {},
) {
val intent = Intent(this, T::class.java)
intent.block()
this.startActivity(intent)
}
这样,就可以在启动 TestActivity
的同时传递数据了:
kotlin
// 在 Activity 或 Fragment 中
startActivity<TestActivity>(context) {
putExtra("param1", "data")
putExtra("param2", 123)
}
型变:协变与逆变
Kotlin 泛型的另一个强大特性是型变 ,它描述了泛型类型之间的子类型关系。要理解型变,先要了解 in
和 out
位置。
在泛型类中,如果一个泛型类型 T
只在函数的返回值或只读属性(使用 val
声明)的类型中出现,我们就称 T
处于 out
位置。如果 T
只在函数的参数类型中出现,我们就称 T
处于 in
位置。
你也可以记为:out 对应生产者(Producer),只生产(输出)数据;in 对应消费者(Consumer),只消费(接收)数据。
知道了这个后,我们继续。
协变(out)
请你看看下面的这段代码:
kotlin
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
对象。那么,如果一个接收 List<Person>
类型参数的方法,可以传入 List<Student>
对象吗?
在 Java 中是不行的,因为 List<Student>
并不是 List<Person>
的子类型,可能会导致类型安全问题。
假设可以的话,请看下面的例子:
kotlin
class SimpleData<T> {
private var data: T? = null
fun set(t: T?) {
data = t
}
fun get(): T? {
return data
}
}
fun main() {
val student = Student("Tom", 19)
val data = SimpleData<Student>()
data.set(student)
handleSimpleData(data) // 实际上这行代码会报错,但我们假设它能编译通过
val studentData: Student? = data.get()
}
fun handleSimpleData(data: SimpleData<Person>) {
val teacher = Teacher("Jack", 35)
data.set(teacher)
}
在 handleSimpleData()
方法中,我们将一个 Teacher
实例存放到了 SimpleData<Person>
对象的 data
属性中,这是完全没问题的。但在 main()
函数中,data
变量的类型是 SimpleData<Student>
,我们调用 get()
方法期望获取它内部封装的 Student
对象,但实际上得到的却是一个 Teacher
对象,会导致 ClassCastException
类型转换异常。
为了杜绝这种因写入操作导致的类型安全问题,Kotlin 规定,如果泛型类是可读可写的,那么 SimpleData<Student>
和 SimpleData<Person>
之间没有任何继承关系,SimpleData<Student>
并不是 SimpleData<Person>
的子类型。
不过,如果我们保证泛型类是只读的,就没有这种问题了。这就是协变的作用。
泛型协变的概念是:对于泛型类 MyClass<T>
,如果 A
是 B
的子类,同时 MyClass<A>
又是 MyClass<B>
的子类,那么 MyClass
在泛型 T
上是协变的。
如何实现呢?需要使用 out
关键字,并确保泛型 T
只能出现在 out
位置,在上述例子中,就是让 data
为只读属性。
我们来对 SimpleData
类进行改造:
kotlin
class SimpleData<out T>(private val data: T?) {
fun get(): T? {
return data
}
}
我们在泛型 T
的声明前加上了 out
关键字,并且将 data
属性设置了构造函数的只读参数,移除了 set()
方法。
kotlin
fun main() {
val student = Student("Tom", 19)
val data = SimpleData<Student>(student)
// 现在这行代码可以正常编译了
handleMyData(data)
}
fun handleMyData(data: SimpleData<Person>) {
// 因为 SimpleData 是只读的,所以我们能安全地取出数据
val personData: Person? = data.get()
println(personData?.name)
}
现在,能够向 handleMyData()
方法传递 SimpleData<Student>
对象了。
我们前面说过:"在 Java 中,如果某个方法接收一个 List<Person>
类型的参数,我们是不允许传入 List<Student>
实例的。"但在 Kotlin 中是允许的,你并没有猜错,因为 Kotlin 给很多内置的 API 加上了协变的声明,就比如 List<E>
接口。
我们来看看 List
的源码:
kotlin
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>
}
你会发现,尽管 List
在泛型 E
上是协变的(使用了 out 关键字声明),但泛型 E
还是出现在了函数类型中(in
位置),例如 contains
方法。
这是因为这些方法并不会修改集合内容,所以这些操作是类型安全的。为了告诉编译器我们的用法是安全的,我们在泛型 E
前加上了 @UnsafeVariance
注解。
逆变(in)
逆变则与协变完全相反,其定义为:**对于泛型类 MyClass<T>
,如果 A
是 B
的子类,但 MyClass<B>
反而被看作 MyClass<A>
的子类,那么 MyClass
在泛型 T
上是逆变的。
你可能会觉得:MyClass<A>
是 MyClass<B>
的子类型我能理解,但MyClass<B>
是 MyClass<A>
的子类型又是怎么回事?别急,我们来看一个实际例子。
假设有一个 Transformer
转换器接口,用于将一个对象转为字符串。
kotlin
interface Transformer<in T> {
fun transform(t: T): String
}
其中的 in
关键字约束了 T
只能出现在 in
位置,并声明了 Transformer
在泛型 T
上是逆变的。我们来看下面的用法:
kotlin
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)
fun main() {
// 这个转换器可以处理任何 Person
val personTransformer = object : Transformer<Person> {
override fun transform(t: Person): String {
return "Name: ${t.name}, Age: ${t.age}"
}
}
handleStudent(personTransformer)
}
fun handleStudent(trans: Transformer<Student>) {
val student = Student("Tom", 19)
val result = trans.transform(student)
println(result)
}
现在编译可正常通过,为什么 handleStudent()
方法可以接收 Transformer<Person>
类型的参数?
因为逆变,此时,Transformer<Person>
成为了 Transformer<Student>
的子类型。换一种说法,一个能处理 Person
类的转换器,也一定能处理其子类 Student
。
当然,你可以再思考一下,如果逆变允许泛型 T
出现在 out
位置,会发生什么安全问题?
修改 Transformer
接口,并使用 @UnsafeVariance
注解强行让它通过编译:
kotlin
interface Transformer<in T> {
fun transform(name: String, age: Int): @UnsafeVariance T
}
然后是示例代码:
kotlin
fun main() {
// 实现转换器,返回一个 Teacher 实例
val trans = object : Transformer<Person> {
override fun transform(name: String, age: Int): Person {
return Teacher(name, age)
}
}
handleTransformer(trans)
}
fun handleTransformer(trans: Transformer<Student>) {
// 希望获得一个 Student 实例
val result: Student = trans.transform("Tom", 19)
}
在上述代码中,handleTransformer()
方法期望得到的是一个 Student
对象,但 trans.transform()
方法实际返回的是一个 Teacher
实例,那么在赋值的时候,就会出现 ClassCastException
类型转换异常
Kotlin 在提供协变和逆变时,就已经把各种潜在的类型转换安全隐患全部考虑到了。只要我们严格按照其语法规则,让泛型在协变时只出现在 out
位置,逆变时只出现在 in
位置,就不会存在类型转换异常的风险。
虽然
@UnsafeVariance
注解可以强行突破这个规则,但同时也产生了风险。
最后介绍一下逆变的应用场景,比如用于比较大小的 Comparable
接口,其源码如下:
kotlin
public interface Comparable<in T> {
public operator fun compareTo(other: T): Int
}
Comparable
在 T
这个泛型上就是逆变的,因为一个能和 Person
比较大小的逻辑,也一定能用来比较 Student
。