10.6--Kotlin 课堂:泛型的高级特性

还记得在第8章的Kotlin 课堂里我们学习的Kotlin 泛型的基本用法吗?这些基本用法其实和Java 中泛型的用法是大致相同的,因此也相对比较好理解。然而实际上,Kotlin 在泛型方面还提供了不少特有的功能,掌握了这些功能,你将可以更好玩转Kotlin,同时还能实现一些不可思议的语法特性,那么我们自然不能错过这部分内容了。

 

10.6.1 对泛型进行实化

泛型实化这个功能对于绝大多数Java 程序员来将是非常陌生的,因为Java 中完全没有这个概念。而如果我们想要深刻地理解泛型实化,就要先解释一下Java 的泛型擦除机制才行。

在JDK1.5 之前,Java 是没有泛型功能的,那个时候诸如List 之类的数据结构可以存储任意类型数据,取出数据的时候也需要手动向下转型才行,这不仅麻烦,而且很危险。比如说我们在同一个List 中存储了字符串和整型两种数据,但是在取出数据的时候却无法区分具体的数据类型,如果手动将它们强制转成同一类型,那么就会抛出类型转换异常。

于是在JDK1.5 中,Java 终于引入了泛型功能。这不仅让诸如List 之类的数据结构变得简单好用,也让我们的代码变得更加安全。

但是实际上,Java 的泛型功能是通过类型擦除机制来实现的。什么意思呢?就是说泛型对于类型的约束只在编译器时期存在,运行的时候仍然会按照JDK1.5 之前的机制来运行,JVM 是识别不出来玩吗在代码中指定的泛型类型的。例如,假设我们创建了一个List<String> 集合,虽然在编译时期只能向集合中添加字符串类型的元素,但是在运行时期JVM 并不能知道它本来只打算包含哪种类型的元素,只能识别出来它是个List。

所有基于JVM 的语言,它们的泛型功能都是通过类型擦除机制来实现的,其中当然也包括了Kotlin。这种机制使得我们不可能使用a is T 或者 T :: class.java 这样的语法,因为T的实际类型在运行的时候已经被擦除了。

然而不同的是,Kotlin 提供了一个内联函数的概念,我们在第6章的Kotlin 课堂中已经学过了这个知识点。内联函数中的代码会在编译的时候自动被替换到调用它的地方,这样的话也就不存在什么泛型擦除的问题了,因为代码在编译之后会直接使用实际的类型来替代内联函数中的泛型声明,如下所示:

fun foo(){
    bar<String>()
}

inline fun<T> bar(){
    // do something with T type
}

最终代码会被替换成以下的样子:

fun foo(){
    // do something with String type
}

可以看到,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的类型。

关于泛型实例化的基本用法就介绍到这里,接下来我们看一看,泛型实化在Android 项目当中具体可以有哪些应用。

 

10.6.2 泛型实化的应用

泛型实化功能允许我们在泛型函数当中获得泛型的实际类型,这也就使得类似于a is T、T::class.java 这样的语法成为了可能。而灵活的运用这一特性可以实现一些不可思议的语法结构,下面我们赶快来看一下吧。

到目前为止,我们已经将Android 的四大组件全部学习完了,除了ContentProvider 之外,你会发现其余3个组件有一个共同的特点,它们都是要结合Intent 一起使用的。比如说启动一个Activity 就可以这么写;

            val intent = Intent(this,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 参数,并同时使用inlinereified 关键字让泛型T 成为了一个被实化的泛型。接下来就是神奇的地方了,Intent 接收的第二个参数本来应该是一个具体Activity 的 Class 类型,但由于现在T 已经是一个被实化的泛型了,因此这里我们可以直接传入T::class.java。最后调用Context 的 startActivity()方法来完成Activity 的启动。

现在,如果我们想要启动TestActivity,只需要这样写就可以了。

    startActivity<TestActivity>(context)

Kotlin 将能够识别出指定泛型的实际类型,并启动相应的Activity 。 怎么样,是不是觉得代码瞬间精简了好多?这就是泛型实化所带来的神奇功能。

不过,现在的startActivity() 函数其实还是有问题的,因为通常在启用Activity 的时候还可能会使用Intent 附带一些参数,比如下面的写法:

    val intent = Intent(context,T::class.java)
    intent.putExtra("param1","data")
    intent.putExtra("param2",123)
    context.startActivity(intent)

而经过刚才的封装之后,我们就无法进行传参了。

这个问题也不难解决,只需要借助之前在第6章学习的高阶函数就可以轻松搞定。回到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 提供了如此多优秀的语言特性。

好了,泛型实化的具体应用学到这里就基本结束了。虽然我们一直在使用启动Activity 的代码来举例,但是启动Service 的代码也是基本类似的,相对于你来说,通过泛型实化和高阶函数来简化它的用法已经是小菜一碟了,这个功能就当做课后练习题让你练练手吧。

那么接下来我们继续学习泛型更多的高级特性。

 

10.6.3 泛型的协变

泛型的协变和逆变功能不太常用,而且我个人认为有点不太容易理解。但是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<Person> 类型的参数,而我们传入一个List<Student>的实例,这样合不合法呢?看上去好像也挺正常的,但是Java 中是不允许这么做的,因为List<Student> 不能成为List<Person>的子类,否则将可能存在类型转换的安全隐患(比如子类有的属性或方法,父类有可能没有)。

为什么会存在类型转换的安全隐患呢?下面我们通过一个具体的例子进行说明。这里自定义一个SimpleData类,代码如下所示:

class SimpleData<T>{
    private val data:T? = null
    fun set(t:T?){

    }
    fun get():T?{
        return data
    }
}

SimpleData 是一个泛型类,它的内部封装了一个泛型data 字段,调用set() 方法可以给data 字段赋值,调用get() 方法可以获取data 字段的值。

接着我们假设,如果编程语言允许向某个接收SimpleData<Person>参数的方法传入SimpleData<Student> 的实例,那么如下代码就会是合法的:

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",35)
    data.set(teacher)
}

发现这段代码有什么问题吗?在main() 方法中,我们创建了一个Student 的实例,并将它封装到SimpleData<Student> 当中,然后将SimpleData<Student> 作为参数传递给handleSimpleData() 方法。但是handleSimpleData() 方法接收的是一个SimpleData<Person>参数(这里假设可以编译通过),那么在handleSimpleData() 方法中,我们就可以创建一个Teacher 的实例,并用它来替换SimpleData<Person>参数中的原有数据。这种操作肯定是合法的,因为Teacher 也是Person 的子类,所以可以很安全第将Teacher 的实例设置进去。

但是问题马上来了,回到main() 方法当中,我们调用SimpleData<Student> 的get() 方法来获取它内部封装的Student 数据,可现在SimpleData<Student>中实际包含的却是一个Teacher 的实例,那么此时必然会产生类型转换异常(Student 和 Teacher 类虽然都继承了Person,但是他俩并没有关系,不能转换)。

所以,为了杜绝这种安全隐患,Java 是不允许这种方式来传递参数的。换句话说,即使Student 是 Person 的子类,SimpleData<Student> 并不是SimpleData<Person>的子类。

不过,回顾一下刚才的代码,你会发现问题发生的主要原因是我们在handleSimpleData() 方法中想SimpleData<Person> 里设置了一个Teacher 的实例。如果SimpleData 在泛型T 上是只读的话,肯定就没有类型转换的安全隐患了,那么这个时候SimpleData<Student>可不可以成为SimpleData<Person> 的子类呢?

讲到这里,我们终于要引出泛型协变的定义了。假如定义了一个MyClass<T>的泛型函数,其中A 是B 的子类型,同时MyClass<A> 又是 MyClass<B> 的子类型,那么我们就可以称MyClass 在T这个泛型上是协变的。

但是如何才能MyClass<A> 成为 MyClass<B> 的子类型呢?刚才已经讲了,如果是一个泛型类在其泛型类型的数据上是只读的话,那么它是没有类型转换的安全隐患的。而要实现这一点,则需要让MyClass<T> 类中的所有方法都不能接收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 上是协变的。(这个out 就是协变声明)

由于泛型T 不能出现在in 位置上,因此我们也就不能使用set() 方法为data 参数赋值了,所以这里改成了使用构造函数的方式来赋值。你可能会说,构造函数中的泛型T 不也是在in 位置上的吗?没错,但是由于这里我们使用了val 关键字,所以构造函数中的泛型T 仍然是只读的,因此这样写是合法且安全的。另外,即使我们使用了var 关键字,但只要给它加上private 修饰符,保证这个泛型T 对于外部而言是不可修改的,那么就都是合法的写法。

经过了这样的的修改之后,下面的代码就可以完美编译通过且没有任何安全隐患了:

fun main() {
    val student = Student("Tom",18)
    val data = SimpleData<Student>(student)
    handleSimpleData(data) // 实际上这行代码会报错,这里假设它能编译通过
    val studentData = data.get()

}
fun handleSimpleData(data:SimpleData<Person>){
    val personData = data.get()
}

由于SimpleData 类已经进行了协变声明,那么SimpleData<Student> 自然就是SimpleData<Person>的子类了,所以这里可以安全地像handleMyData() 方法中传递参数。

然后在handleMyData() 方法中去获取SimpleData 封装的数据,虽然这里泛型声明的是Person 类型,实际获得的会是一个Student 的实例,但由于Person 是Student 的父亲,向上转型是完全安全的,所以这段代码没有任何问题。

学到这里,关于协变的内容你就掌握得差不多了,不过最后还有一个例子需要回顾一下。前面我们提到,如果某个方法接收一个List<Person>类型的参数,而传入的却是一个List<Student> 的实例,在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 对此是不负责的。

好了,关于协变的内容就学到这里,接下来我们开始学习逆变的内容。

 

10.6.4 泛型的逆变

理解了协变之后再来学习逆变,我觉得会相对比较容易一些,因为它们之间是有所关联的。

不过仅从定义上来看,逆变与协变却完全相反。那么这里先引出定义吧,假如定义了一个MyClass<T> 的泛型类,其中A 是B 的子类型,同时MyClass<B> 又是 MyClass<A> 的子类型,那么我们就可以称MyClass 在 T 这个泛型上是逆变的。协变和逆变的区别如图:

从直观的角度上来思考,逆变的规则好像挺奇怪的,原本A 是B 类型的子类型,怎么MyClass<B> 能反过来成为MyClass<A> 的子类型了呢?别担心,下面我们通过一个具体的例子来学习一下,你就明白了。

这里先定义一个Transformer 接口,用于执行一些转换操作,代码如下所示:

interface Transformer<T>{
    fun transform(t:T):String
}

可以看到,Transformer 接口中声明了一个transform() 方法,它接收一个T 类型的参数,并且返回一个String 类型的数据,这意味着参数T 在经过 transform() 方法的转换之后将会变成一个字符串。至于具体的转换逻辑是什么条件是什么样的,则由子类去实现,Transformer 接口对此并不关心。

那么现在我们就尝试对 Transformer 接口进行实现,代码如下所示:

fun handleTransformer(trans:Transformer<Student>){
    val student = Student("Tom",19)
    val result = trans.transform(student)
}

fun main() {
    val trans = object : Transformer<Person>{
        override fun transform(t: Person): String {
            return "${t.name} ${t.age}"
        }
    }
    handleTransformer(trans) // 这行代码会报错。
}

首先我们在main() 方法中编写了一个Transformer<Person> 的匿名类实现,并通过transform() 方法将传入的Person 对象转换成了一个"姓名+年龄" 拼接的字符串。而handleTransformer() 方法接收的是一个Transformer<Student> 类型的参数,这里在handleTransformer() 方法中创建了一个Student 对象,并调用参数的transform() 方法将Student 对象转换成一个字符串。

这段代码从安全的角度来分析是没有任何问题的,因为Student 是 Person 的子类,使用Transformer<Person> 的匿名类实现将 Student 对象转换成一个字符串也是绝对安全的,并不存在类型转换的安全隐患。但是实际上,在调用handleTransformer() 方法的时候却会提示语法错误,原因也很简单,Transformer<Person> 并不是Transformer<Student> 的子类型。

那么这个时候逆变就可以派上用场了,它就是专门用于处理这种情况的。修改Transformer 接口中的代码,如下所示:

interface Transformer<in T>{
    fun transform(t:T):String
}

这里我们在泛型T 的声明长加了一个in 关键字 。 这就意味着现在T 只能出现在in 位置上,而不能出现在out 位置上,同时也意味着Transformer 在泛型T 上是逆变的。(逆变声明)

没错,只要做了这样一点修改,刚才的代码可以编译通过且正常运行了,因为此时Transformer<Person> 已经成为了Transformer<Student> 的子类型。

逆变的用法大概就是这样了,如果你还想再深入思考一下的话,可以想一想为什么逆变的时候泛型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 handleTransformer(trans:Transformer<Student>){
    val result = trans.transform("Tom",19)
}

fun main() {
    val trans = object : Transformer<Person>{
        override fun transform(name: String, age: Int): Person {
            return Teacher(name,age)
        }
    }
    handleTransformer(trans) 
}

上诉代码就是一个典型的违反逆变规则而造成类型转换异常的例子。在Transformer<Person> 的匿名实现类中,我们使用 transform() 方法中传入了name 和 age 参数构建了一个Teacher 对象,并把这个对象直接返回。由于 transform() 方法的返回值要求是一个Person 对象,而Teacher 是Person 的子类,因此这种写法肯定是合法的。

但在handleTransformer() 方法当中,我们调用了Transformer<Student> 的 transform() 方法,并传入了name 和 age 这两个参数,期望得到的是一个Student 对象的返回,然而实际上transform() 方法返回的却是一个Teacher 对象,因此这里必然会造成类型转换异常。

由于这段代码是可以编译通过的,那么我们可以运行一下,打印出的异常信息如图:

Exception in thread "main" java.lang.ClassCastException: com.example.servicetest.Teacher cannot be cast to com.example.servicetest.Student
	at com.example.servicetest.TransformKt.handleTransformer(Transform.kt:12)
	at com.example.servicetest.TransformKt.main(Transform.kt:21)
	at com.example.servicetest.TransformKt.main(Transform.kt)

可以看到,提示我们Teacher 类型是无法转换成Student 类型的。

也就是说,Kotlin 在提供协变和逆变功能时,就以及把各种潜在的类型转换安全隐患全部考虑进去了,只要我们严格按照其语法规则,让泛型在协变时只出现在out 位置上,逆变时只出现在in 位置上,就不会存在类型转换异常的情况。虽然@UnsafeVariance 注解可以打破这一语法规则,但同事也会带来额外的风险,所以你在使用@UnsafeVariance 注解时,必须很清楚自己在干什么才行。

最后我们再来介绍一下逆变功能在Kotlin 内置API 中的应用,比较典型的例子就是Comparable 的使用。Comparable 是一个用于比较两个对象大小的接口,其源码定义如下:

interface Comparator<in T>{
    operator fun compareTo(other:T):Int
}

可以看到,Comparable 在T 这个类型上就是逆变的,compareTo() 方法则用于实现具体的比较逻辑,那么这里为什么要让Comparable 接口是逆变的呢?想象如下场景,如果我们使用Comparable<Person> 实现了让两个Person 对象比较大小的逻辑,那么用这段逻辑去比较两个Student 对象的大小也一定是成立的,因此我让Comparable<Person> 成为Comparable<Student>的子类合情合理,这就是逆变非常典型的应用。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值