对泛型进行实化
泛型实化这个功能对于绝大多数Java 程序员来讲是非常陌生的,因为Java 中完全没有这个概
念。而如果我们想要深刻地理解泛型实化,就要先解释一下Java 的泛型擦除机制才行。
在JDK 1.5之前,Java 是没有泛型功能的,那个时候诸如List之类的数据结构可以存储任意类型
的数据,取出数据的时候也需要手动向下转型才行,这不仅麻烦,而且很危险。比如说我们在
同一个List中存储了字符串和整型这两种数据,但是在取出数据的时候却无法区分具体的数据类
型,如果手动将它们强制转成同一种类型,那么就会抛出类型转换异常。
于是在JDK 1.5中,Java 终于引入了泛型功能。这不仅让诸如List之类的数据结构变得简单好
用,也让我们的代码变得更加安全。
但是实际上,Java 的泛型功能是通过类型擦除机制来实现的。什么意思呢?就是说泛型对于类
型的约束只在编译时期存在,运行的时候仍然会按照JDK 1.5之前的机制来运行,JVM是识别不
出来我们在代码中指定的泛型类型的。例如,假设我们创建了一个List集合,虽然
在编译时期只能向集合中添加字符串类型的元素,但是在运行时期JVM并不能知道它本来只打算
包含哪种类型的元素,只能识别出来它是个List。
所有基于JVM的语言,它们的泛型功能都是通过类型擦除机制来实现的,其中当然也包括了
Kotlin 。这种机制使得我们不可能使用a is T或者T::class.java这样的语法,因为T的实际
类型在运行的时候已经被擦除了。
然而不同的是,Kotlin 提供了一个内联函数的概念,我们在第6章的Kotlin 课堂中已经学过了这
个知识点。内联函数中的代码会在编译的时候自动被替换到调用它的地方,这样的话也就不存
在什么泛型擦除的问题了,因为代码在编译之后会直接使用实际的类型来替代内联函数中的泛
型声明,其工作原理如图
可以看到,bar()是一个带有泛型类型的内联函数,foo()函数调用了bar()函数,在代码编
译之后,bar()函数中的代码将可以获得泛型的实际类型。
这就意味着,Kotlin 中是可以将内联函数中的泛型进行实化的。
那么具体该怎么写才能将泛型实化呢?首先,该函数必须是内联函数才行,也就是要用inline
关键字来修饰该函数。其次,在声明泛型的地方必须加上reified关键字来表示该泛型要进行
实化。示例代码如下:
inline fun <reified T> getGenericType() {
}
上述函数中的泛型T就是一个被实化的泛型,因为它满足了内联函数和reified关键字这两个前
提条件。那么借助泛型实化,到底可以实现什么样的效果呢?从函数名就可以看出来了,这里
我们准备实现一个获取泛型实际类型的功能,代码如下所示:
inline fun <reified T> getGenericType() = T::class.java
虽然只有一行代码,但是这里却实现了一个Java 中完全不可能实现的功能:
getGenericType()函数直接返回了当前指定泛型的实际类型。T.class这样的语法在Java
中是不合法的,而在Kotlin 中,借助泛型实化功能就可以使用T::class.java这样的语法了。
现在我们可以使用如下代码对getGenericType()函数进行测试:
fun main() {
val result1 = getGenericType<String>()
val result2 = getGenericType<Int>()
println("result1 is $result1")
println("result2 is $result2")
}
这里给getGenericType()函数指定了两种不同的泛型,由于getGenericType()函数会将
指定泛型的具体类型返回,因此这里我们将返回的结果进行打印。
现在运行一下main()函数,结果如图
泛型实化功能的运行结果
可以看到,如果将泛型指定成了String,那么就可以得到java.lang.String的类型;如果
将泛型指定了Int,就可以得到java.lang.Integer的类型。
关于泛型实化的基本用法就介绍到这里,接下来我们看一看,泛型实化在Andr oid 项目当中具体
可以有哪些应用。
泛型实化的应用
泛型实化功能允许我们在泛型函数当中获得泛型的实际类型,这也就使得类似于a is T、
T::class.java这样的语法成为了可能。而灵活运用这一特性将可以实现一些不可思议的语法
结构,下面我们赶快来看一下吧。
到目前为止,我们已经将Andr oid 的四大组件全部学完了,除了ContentP rovider 之外,你会
发现其余的3个组件有一个共同的特点,它们都是要结合Intent 一起使用的。比如说启动一个
Activity 就可以这么写:
val intent = Intent(context, TestActivity::class.java)
context.startActivity(intent)
有没有觉得TestActivity::class.java这样的语法很难受呢?当然,如果在没有更好选择
的情况下,这种写法也是可以忍受的,但是Kotlin 的泛型实化功能使得我们拥有了更好的选择。
新建一个reified.kt 文件,然后在里面编写如下代码:
inline fun <reified T> startActivity(context: Context) {
val intent = Intent(context, T::class.java)
context.startActivity(intent)
}
这里我们定义了一个startActivity()函数,该函数接收一个Context参数,并同时使用
inline和reified关键字让泛型T成为了一个被实化的泛型。
接下来就是神奇的地方了,Intent 接收的第二个参数本来应该是一个具体Activity 的Class类型,但由于现在T已经是一个
被实化的泛型了,因此这里我们可以直接传入T::class.java。最后调用Context的
startActivity()方法来完成Activity 的启动。
现在,如果我们想要启动TestA ctivity ,只需要这样写就可以了:
startActivity<TestActivity>(context)
Kotlin 将能够识别出指定泛型的实际类型,并启动相应的Activity 。怎么样,是不是觉得代码瞬
间精简了好多?这就是泛型实化所带来的神奇功能。
不过,现在的startActivity()函数其实还是有问题的,因为通常在启用Activity 的时候还可
能会使用Intent 附带一些参数,比如下面的写法:
val intent = Intent(context, TestActivity::class.java)
intent.putExtra("param1", "data")
intent.putExtra("param2", 123)
context.startActivity(intent)
而经过刚才的封装之后,我们就无法进行传参了。
这个问题也不难解决,只需要借助高阶函数就可以轻松搞定。回到
reified.kt 文件当中,这里添加一个新的startActivity()函数重载,如下所示:
inline fun <reified T> startActivity(context: Context, block: Intent.() -> Unit) {
val intent = Intent(context, T::class.java)
intent.block()
context.startActivity(intent)
}
可以看到,这次的startActivity()函数中增加了一个函数类型参数,并且它的函数类型是
定义在Intent 类当中的。在创建完Intent 的实例之后,随即调用该函数类型参数,并把Intent 的
实例传入,这样调用startActivity()函数的时候就可以在Lambda 表达式中为Intent 传递
参数了,如下所示:
startActivity<TestActivity>(context) {
putExtra("param1", "data")
putExtra("param2", 123)
}
不得不说,这种启动Activity 的代码写起来实在是太舒服了,泛型实化和高阶函数使这种语法结
构成为了可能,感谢Kotlin 提供了如此多优秀的语言特性。
泛型的协变
泛型的协变和逆变功能不太常用,而且我个人认为有点不容易理解。但是Kotlin 的内置API中使
用了很多协变和逆变的特性,因此如果想要对这个语言有更加深刻的了解,这部分内容还是有
必要学习一下的。
我在学习协变和逆变的时候查阅了很多资料,这些资料大多十分晦涩难懂,因此也让我对这两
个知识点产生了一些畏惧。但是真正掌握之后,发现其实也并不是那么难,所以这里我会尽量
使用最简明的方式来讲解这两个知识点,希望你可以轻松掌握。
在开始学习协变和逆变之前,我们还得先了解一个约定。一个泛型类或者泛型接口中的方法,
它的参数列表是接收数据的地方,因此可以称它为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类,类中包含name和age这两个字段。然后又定义了Student和
Teacher这两个类,让它们成为Person类的子类。
现在我来问你一个问题:如果某个方法接收一个Person类型的参数,而我们传入一个Student
的实例,这样合不合法呢?很显然,因为Student是Person的子类,学生也是人呀,因此这是
一定合法的。
那么我再来升级一下这个问题:如果某个方法接收一个List类型的参数,而我们传
入一个List的实例,这样合不合法呢?看上去好像也挺正确的,但是Java 中是不
允许这么做的,因为List不能成为List的子类,否则将可能存在类型
转换的安全隐患。
为什么会存在类型转换的安全隐患呢?下面我们通过一个具体的例子进行说明。这里自定义一
个SimpleData类,代码如下所示:
class SimpleData<T> {
private var data: T? = null
fun set(t: T?) {
data = t
}
fun get(): T? {
return data
}
}
SimpleData是一个泛型类,它的内部封装了一个泛型data字段,调用set()方法可以给data
字段赋值,调用get()方法可以获取data字段的值。
接着我们假设,如果编程语言允许向某个接收SimpleData参数的方法传入
SimpleData的实例,那么如下代码就会是合法的:
fun main() {
val student = Student("Tom", 19)
val data = SimpleData<Student>()
data.set(student)
handleSimpleData(data) // 实际上这行代码会报错,这里假设它能编译通过
val studentData = data.get()
}
fun handleSimpleData(data: SimpleData<Person>) {
val teacher = Teacher("Jack", 35)
data.set(teacher)
}
发现这段代码有什么问题吗?在main()方法中,我们创建了一个Student的实例,并将它封装
到SimpleData当中,然后将SimpleData作为参数传递给
handleSimpleData()方法。但是handleSimpleData()方法接收的是一个
SimpleData参数(这里假设可以编译通过),那么在handleSimpleData()方法
中,我们就可以创建一个Teacher的实例,并用它来替换SimpleData参数中的原
有数据。这种操作肯定是合法的,因为Teacher也是Person的子类,所以可以很安全地将
Teacher的实例设置进去。
但是问题马上来了,回到main()方法当中,我们调用SimpleData的get()方法
来获取它内部封装的Student数据,可现在SimpleData中实际包含的却是一个
Teacher的实例,那么此时必然会产生类型转换异常。
所以,为了杜绝这种安全隐患,Java 是不允许使用这种方式来传递参数的。换句话说,即使
Student是Person的子类,SimpleData并不是SimpleData的子
类。
不过,回顾一下刚才的代码,你会发现问题发生的主要原因是我们在handleSimpleData()方
法中向SimpleData里设置了一个Teacher的实例。如果SimpleData在泛型T上是
只读的话,肯定就没有类型转换的安全隐患了,那么这个时候SimpleData可不可
以成为SimpleData的子类呢?
讲到这里,我们终于要引出泛型协变的定义了。假如定义了一个MyClass的泛型类,其中A
是B的子类型,同时MyClass< A > 又是 MyClass< B > 的子类型,那么我们就可以称 MyClass 在T
这个泛型上是协变的。
但是如何才能让 MyClass< A > 成为 MyClass< B > 的子类型呢?
刚才已经讲了,如果一个泛型类在其泛型类型的数据上是只读的话,那么它是没有类型转换安全隐患的。而要实现这一点,则
需要让MyClass类中的所有方法都不能接收T类型的参数。换句话说,T只能出现在out 位置
上,而不能出现在in位置上。
现在修改SimpleData类的代码,如下所示:
class SimpleData<out T>(val data: T?) {
fun get(): T? {
return data
}
}
这里我们对SimpleData类进行了改造,在泛型T的声明前面加上了一个out关键字。这就意味
着现在T只能出现在out 位置上,而不能出现在in位置上,同时也意味着SimpleData在泛型T上
是协变的。
由于泛型T不能出现在in位置上,因此我们也就不能使用set()方法为data参数赋值了,所以这
里改成了使用构造函数的方式来赋值。你可能会说,构造函数中的泛型T不也是在in位置上的
吗?没错,但是由于这里我们使用了val关键字,所以构造函数中的泛型T仍然是只读的,因此
这样写是合法且安全的。另外,即使我们使用了var关键字,但只要给它加上private修饰
符,保证这个泛型T对于外部而言是不可修改的,那么就都是合法的写法。
经过了这样的修改之后,下面的代码就可以完美编译通过且没有任何安全隐患了:
fun main() {
val student = Student("Tom", 19)
val data = SimpleData<Student>(student)
handleMyData(data)
val studentData = data.get()
}
fun handleMyData(data: SimpleData<Person>) {
val personData = data.get()
}
由于SimpleData类已经进行了协变声明,那么SimpleData自然就是
SimpleData的子类了,所以这里可以安全地向handleMyData()方法中传递参
数。
然后在handleMyData()方法中去获取SimpleData封装的数据,虽然这里泛型声明的是
Person类型,实际获得的会是一个Student的实例,但由于Person是Student的父类,向上
转型是完全安全的,所以这段代码没有任何问题。
学到这里,关于协变的内容你就掌握得差不多了,不过最后还有个例子需要回顾一下。前面我
们提到,如果某个方法接收一个List类型的参数,而传入的却是一个
List的实例, 在Java 中是不允许这么做的。注意这里我的用语,在Java 中是不允
许这么做的。
你没有猜错,在Kotlin 中这么做是合法的,因为Kotlin 已经默认给许多内置的API加上了协变声
明,其中就包括了各种集合的类与接口。还记得我们在第2章中学过的吗?Kotlin 中的List本身
就是只读的,如果你想要给List添加数据,需要使用MutableList 才行。既然List是只读的,也
就意味着它天然就是可以协变的,我们来看一下List简化版的源码:
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>
public operator fun get(index: Int): E
}
List在泛型E的前面加上了out关键字,说明List在泛型E上是协变的。不过这里还有一点需要说
明,原则上在声明了协变之后,泛型E就只能出现在out 位置上,可是你会发现,在
contains()方法中,泛型E仍然出现在了in位置上。
这么写本身是不合法的,因为在in位置上出现了泛型E就意味着会有类型转换的安全隐患。但是
contains()方法的目的非常明确,它只是为了判断当前集合中是否包含参数中传入的这个元
素,而并不会修改当前集合中的内容,因此这种操作实质上又是安全的。那么为了让编译器能
够理解我们的这种操作是安全的,这里在泛型E的前面又加上了一个@UnsafeVariance注解,
这样编译器就会允许泛型E出现在in位置上了。但是如果你滥用这个功能,导致运行时出现了类
型转换异常,Kotlin 对此是不负责的。
泛型的逆变
理解了协变之后再来学习逆变,我觉得会相对比较容易一些,因为它们之间是有所关联的。
不过仅从定义上来看,逆变与协变却完全相反。那么这里先引出定义吧,假如定义了一个
MyClass < T >的泛型类,其中A是B的子类型,同时 MyClass< B > 又是 MyClass < A >的子类型,
那么我们就可以称MyClass在T这个泛型上是逆变的。协变和逆变的区别如图
协变与逆变的区别
从直观的角度上来思考,逆变的规则好像挺奇怪的,原本A是B的子类型,怎么MyClass能
反过来成为MyClass的子类型了呢?别担心,下面我们通过一个具体的例子来学习一下,
你就明白了。
这里先定义一个Transformer接口,用于执行一些转换操作,代码如下所示:
interface Transformer<T> {
fun transform(t: T): String
}
可以看到,Transformer接口中声明了一个transform()方法,它接收一个T类型的参数,并
且返回一个String类型的数据,这意味着参数T在经过transform()方法的转换之后将会变成
一个字符串。至于具体的转换逻辑是什么样的,则由子类去实现,Transformer接口对此并不
关心。
那么现在我们就尝试对Transformer接口进行实现,代码如下所示:
fun main() {
val trans = object : Transformer<Person> {
override fun transform(t: Person): String {
return "${t.name} ${t.age}"
}
}
handleTransformer(trans) // 这行代码会报错
}
fun handleTransformer(trans: Transformer<Student>) {
val student = Student("Tom", 19)
val result = trans.transform(student)
}
首先我们在main()方法中编写了一个Transformer的匿名类实现,并通过
transform()方法将传入的Person对象转换成了一个“姓名+ 年龄”拼接的字符串。而
handleTransformer()方法接收的是一个Transformer类型的参数,这里在
handleTransformer()方法中创建了一个Student对象,并调用参数的transform()方法
将Student对象转换成一个字符串。
这段代码从安全的角度来分析是没有任何问题的,因为Student是Person的子类,使用
Transformer的匿名类实现将Student对象转换成一个字符串也是绝对安全的,并
不存在类型转换的安全隐患。但是实际上,在调用handleTransformer()方法的时候却会提
示语法错误,原因也很简单,Transformer并不是Transformer的子
类型。
那么这个时候逆变就可以派上用场了,它就是专门用于处理这种情况的。修改Transformer接
口中的代码,如下所示:
interface Transformer<in T> {
fun transform(t: T): String
}
这里我们在泛型T的声明前面加上了一个in关键字。这就意味着现在T只能出现在in位置上,而
不能出现在out 位置上,同时也意味着Transformer在泛型T上是逆变的。
没错,只要做了这样一点修改,刚才的代码就可以编译通过且正常运行了,因为此时
Transformer已经成为了Transformer的子类型。
逆变的用法大概就是这样了,如果你还想再深入思考一下的话,可以想一想为什么逆变的时候
泛型T不能出现在out 位置上?为了解释这个问题,我们先假设逆变是允许让泛型T出现在out 位
置上的,然后看一看可能会产生什么样的安全隐患。
修改Transformer中的代码,如下所示:
interface Transformer<in T> {
fun transform(name: String, age: Int): @UnsafeVariance T
}
可以看到,我们将transform()方法改成了接收name和age这两个参数,并把返回值类型改成
了泛型T。由于逆变是不允许泛型T出现在out 位置上的,这里为了能让编译器正常编译通过,所
以加上了@UnsafeVariance注解,这和List源码中使用的技巧是一样的。
那么,这个时候可能会产生什么样的安全隐患呢?我们来看一下如下代码就知道了:
fun main() {
val trans = object : Transformer<Person> {
override fun transform(name: String, age: Int): Person {
return Teacher(name, age)
}
}
handleTransformer(trans)
}
fun handleTransformer(trans: Transformer<Student>) {
val result = trans.transform("Tom", 19)
}
上述代码就是一个典型的违反逆变规则而造成类型转换异常的例子。在
Transformer的匿名类实现中,我们使用transform()方法中传入的name和age
参数构建了一个Teacher对象,并把这个对象直接返回。由于transform()方法的返回值要求
是一个Person对象,而Teacher是Person的子类,因此这种写法肯定是合法的。
但在handleTransformer()方法当中,我们调用了Transformer的
transform()方法,并传入了name和age这两个参数,期望得到的是一个Student对象的返
回,然而实际上transform()方法返回的却是一个Teacher对象,因此这里必然会造成类型转
换异常。
由于这段代码是可以编译通过的,那么我们可以运行一下,打印出的异常信息如图
逆变使用不当造成的类型转换异常
可以看到,提示我们Teacher类型是无法转换成Student类型的。
也就是说,Kotlin 在提供协变和逆变功能时,就已经把各种潜在的类型转换安全隐患全部考虑进
去了。只要我们严格按照其语法规则,让泛型在协变时只出现在out 位置上,逆变时只出现在in
位置上,就不会存在类型转换异常的情况。虽然@UnsafeVariance注解可以打破这一语法规
则,但同时也会带来额外的风险,所以你在使用@UnsafeVariance注解时,必须很清楚自己
在干什么才行。
最后我们再来介绍一下逆变功能在Kotlin 内置API中的应用,比较典型的例子就是Comparable
的使用。Comparable是一个用于比较两个对象大小的接口,其源码定义如下:
interface Comparable<in T> {
operator fun compareTo(other: T): Int
}
可以看到,Comparable在T这个泛型上就是逆变的,compareTo()方法则用于实现具体的比
较逻辑。那么这里为什么要让Comparable接口是逆变的呢?想象如下场景,如果我们使用
Comparable实现了让两个Person对象比较大小的逻辑,那么用这段逻辑去比较两
个Student对象的大小也一定是成立的,因此让Comparable成为
Comparable的子类合情合理,这也是逆变非常典型的应用。