免费企业建网站,成都口碑最好装修公司,关键词是怎么排名的,网站建设编辑部前面学习了Kotlin中的泛型的基本用法,跟Java中的泛型大致相同#xff0c;Kotlin在泛型方面还提供了不少特有的功能#xff0c;掌握了这些功能#xff0c;你将可以更好玩转Kotlin#xff0c;同时还能实现一些不可思议的语法特性#xff0c;那么我们自然不能错过这部分内容了… 前面学习了Kotlin中的泛型的基本用法,跟Java中的泛型大致相同Kotlin在泛型方面还提供了不少特有的功能掌握了这些功能你将可以更好玩转Kotlin同时还能实现一些不可思议的语法特性那么我们自然不能错过这部分内容了。
1. 对泛型进行实化 Java中完全没有泛型实化这个概念。而如果我们想要深刻地理解泛型实化就要先解释一下Java的泛型擦除机制才行。 在JDK 1.5中Java终于引入了泛型功能。但是实际上Java的泛型功能是通过类型擦除机制来实现的。就是说泛型对于类型的约束只在编译时期存在运行的时候仍然会按照JDK 1.5之前的机制来运行JVM是识别不出来我们在代码中指定的泛型类型的。例如假设我们创建了一List String集合虽然在编译时期只能向集合中添加字符串类型的元素但是在运行时期JVM并不能知道它本来只打算包含哪种类型的元素只能识别出来它是个List。 所有基于JVM的语言它们的泛型功能都是通过类型擦除机制来实现的其中当然也包括了Kotlin。这种机制使得我们不可能使用a is T或者T::class.java这样的语法因为T的实际类型在运行的时候已经被擦除了。 然而不同的是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这样的语法了。
2. 泛型实化的应用 泛型实化功能允许我们在泛型函数当中获得泛型的实际类型这也就使得类似于a is T、T::class.java这样的语法成为了可能。而灵活运用这一特性将可以实现一些不可思议的语法结构. 比如说启动一个Activity就可以这么写
val intent Intent(context, TestActivity::class.java)
context.startActivity(intent)通过Kotlin的泛型实化特性,我们可以进一步进行优化. 新建一个reified.kt文件然后在里面编写如下代码
inline fun reified T startActivity(context: Context) {val intent Intent(context, T::class.java)context.startActivity(intent)
}Intent接收的第二个参数本来应该是一个具体Activity的Class类型但由于现在T已经是一个被实化的泛型了因此这里我们可以直接传入T::class.java。最后调用Context的startActivity()方法来完成Activity的启动。现在如果我们想要启动TestActivity只需要这样写就可以了
startActivityTestActivity(context)通常在启用Activity的时候还可能会使用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()函数的时候就可以在Lambda表达式中为Intent传递参数了如下所示
startActivityTestActivity(context) {putExtra(param1, data)putExtra(param2, 123)
}3. 泛型的协变 泛型的协变和逆变功能不常用,但是在 Kotlin的内置API中用了很多协变和逆变的特性,所以我们可以了解一下,以便更深入的理解Kotlin. 在开始学习协变和逆变之前我们还得先了解一个约定。一个泛型类或者泛型接口中的方法 它的参数列表是接收数据的地方因此可以称它为in位置而它的返回值是输出数据的地方因 此可以称它为out位置如图所示。 先定义了一个Person类类中包含name和age这两个字段。然后又定义了Student和 Teacher这两个类让它们成为Person类的子类。
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的子类否则将可能存在类型转换的安全隐患。 为什么会存在类型转换的安全隐患呢下面我们通过一个具体的例子进行说明。这里自定义一 个SimpleData类代码如下所示
class SimpleDataT {private var data: T? nullfun set(t: T?) {data t}fun get(): T? {return data}
}接着我们假设如果编程语言允许向某个接收SimpleData Person参数的方法传入 SimpleData Student的实例那么如下代码就会是合法的
fun main() {val student Student(Tom, 19)val data SimpleDataStudent()data.set(student)handleSimpleData(data) // 实际上这行代码会报错这里假设它能编译通过val studentData data.get()
}
fun handleSimpleData(data: SimpleDataPerson) {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的实例那么此时必然会产生类型转换异常。所以为了杜绝这种安全隐患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这个泛型上是协变的。 即A,B的继承关系与其对应的泛型类的继承关系是一致的,即为协变. 如何才能让MyClass A成为MyClass B的子类型呢刚才已经讲了如果一个泛型类在其泛型类型的数据上是只读的话那么它是没有类型转换安全隐患的。而要实现这一点则需要让MyClass T类中的所有方法都不能接收T类型的参数。换句话说T只能出现在out位置上而不能出现在in位置上。 修改SimpleData类的代码如下所示
class SimpleDataout 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 SimpleDataStudent(student)handleMyData(data)val studentData data.get()
}
fun handleMyData(data: SimpleDataPerson) {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加上了协变声明其中就包括了各种集合的类与接口。Kotlin中的List本身就是只读的如果你想要给List添加数据需要使用MutableList才行。既然List是只读的也就意味着它天然就是可以协变的我们来看一下List简化版的源码
public interface Listout E : CollectionE {override val size: Intoverride fun isEmpty(): Booleanoverride fun contains(element: UnsafeVariance E): Booleanoverride fun iterator(): IteratorEpublic operator fun get(index: Int): E
}List在泛型E的前面加上了out关键字说明List在泛型E上是协变的。不过这里还有一点需要说明原则上在声明了协变之后泛型E就只能出现在out位置上可是你会发现在contains()方法中泛型E仍然出现在了in位置上。 这么写本身是不合法的因为在in位置上出现了泛型E就意味着会有类型转换的安全隐患。但是 contains()方法的目的非常明确它只是为了判断当前集合中是否包含参数中传入的这个元素而并不会修改当前集合中的内容因此这种操作实质上又是安全的。那么为了让编译器能够理解我们的这种操作是安全的这里在泛型E的前面又加上了一个UnsafeVariance注解这样编译器就会允许泛型E出现在in位置上了。但是如果你滥用这个功能导致运行时出现了类型转换异常Kotlin对此是不负责的。
4. 泛型的逆变 理解了协变之后再来学习逆变会相对比较容易一些因为它们之间是有所关联的。 不过仅从定义上来看逆变与协变却完全相反。那么这里先引出定义吧假如定义了一个 MyClass T的泛型类其中A是B的子类型同时MyClass B又是MyClass A的子类型 那么我们就可以称MyClass在T这个泛型上是逆变的。协变和逆变的区别如图所示。 从直观的角度上来思考逆变的规则好像挺奇怪的原本A是B的子类型怎么MyClass B能 反过来成为MyClass A的子类型了呢下面我们通过一个具体的例子来学习一下。 这里先定义一个Transformer接口用于执行一些转换操作代码如下所示
interface TransformerT {fun transform(t: T): String
}Transformer接口中声明了一个transform()方法它接收一个T类型的参数并且返回一个String类型的数据这意味着参数T在经过transform()方法的转换之后将会变成一个字符串。至于具体的转换逻辑是什么样的则由子类去实现Transformer接口对此并不关心。 现在我们就尝试对Transformer接口进行实现代码如下所示
fun main() {val trans object : TransformerPerson {override fun transform(t: Person): String {return ${t.name} ${t.age}}}handleTransformer(trans) // 这行代码会报错
}
fun handleTransformer(trans: TransformerStudent) {val student Student(Tom, 19)val result trans.transform(student)
}首先我们在main()方法中编写了一个Transformer Person的匿名类实现并通过transform()方法将传入的Person对象转换成了一个“姓名年龄”拼接的字符串。而handleTransformer()方法接收的是一个Transformer Student类型的参数这里在handleTransformer()方法中创建了一个Student对象并调用参数的transform()方法将Student对象转换成一个字符串。 这段代码从安全的角度来分析是没有任何问题的因为Student是Person的子类使用 Transformer Person的匿名类实现将Student对象转换成一个字符串也是绝对安全的并不存在类型转换的安全隐患。但是实际上在调用handleTransformer()方法的时候却会提示语法错误原因也很简单Transformer Person并不是Transformer Student的子类型。 那么这个时候逆变就可以派上用场了它就是专门用于处理这种情况的。修改Transformer接 口中的代码如下所示
interface Transformerin T {fun transform(t: T): String
}这里我们在泛型T的声明前面加上了一个in关键字。这就意味着现在T只能出现在in位置上而 不能出现在out位置上同时也意味着Transformer在泛型T上是逆变的。没错只要做了这样一点修改刚才的代码就可以编译通过且正常运行了因为此时Transformer Person已经成为了Transformer Student的子类型。 逆变的用法大概就是这样了如果你还想再深入思考一下的话可以想一想为什么逆变的时候 泛型T不能出现在out位置上为了解释这个问题我们先假设逆变是允许让泛型T出现在out位 置上的然后看一看可能会产生什么样的安全隐患。 修改Transformer中的代码如下所示
interface Transformerin T {fun transform(name: String, age: Int): UnsafeVariance T
}可以看到我们将transform()方法改成了接收name和age这两个参数并把返回值类型改成了泛型T。由于逆变是不允许泛型T出现在out位置上的这里为了能让编译器正常编译通过所以加上了UnsafeVariance注解这和List源码中使用的技巧是一样的。 那么这个时候可能会产生什么样的安全隐患呢我们来看一下如下代码就知道了
fun main() {val trans object : TransformerPerson {override fun transform(name: String, age: Int): Person {return Teacher(name, age)}}handleTransformer(trans)
}
fun handleTransformer(trans: TransformerStudent) {val result trans.transform(Tom, 19)
}上述代码就是一个典型的违反逆变规则而造成类型转换异常的例子。在Transformer Person的匿名类实现中我们使用transform()方法中传入的name和age参数构建了一个Teacher对象并把这个对象直接返回。由于transform()方法的返回值要求是一个Person对象而Teacher是Person的子类因此这种写法肯定是合法的。 但在handleTransformer()方法当中我们调用了Transformer Student的transform()方法并传入了name和age这两个参数期望得到的是一个Student对象的返回然而实际上transform()方法返回的却是一个Teacher对象因此这里必然会造成类型转换异常。 也就是说Kotlin在提供协变和逆变功能时就已经把各种潜在的类型转换安全隐患全部考虑进 去了。只要我们严格按照其语法规则让泛型在协变时只出现在out位置上逆变时只出现在in位置上就不会存在类型转换异常的情况。虽然UnsafeVariance注解可以打破这一语法规则但同时也会带来额外的风险所以你在使用UnsafeVariance注解时必须很清楚自己在干什么才行。 最后我们再来介绍一下逆变功能在Kotlin内置API中的应用比较典型的例子就是Comparable 的使用。Comparable是一个用于比较两个对象大小的接口其源码定义如下
interface Comparablein T {operator fun compareTo(other: T): Int
}可以看到Comparable在T这个泛型上就是逆变的compareTo()方法则用于实现具体的比较逻辑。那么这里为什么要让Comparable接口是逆变的呢想象如下场景如果我们使用Comparable Person实现了让两个Person对象比较大小的逻辑那么用这段逻辑去比较两个Student对象的大小也一定是成立的因此让Comparable Person成为Comparable Student的子类合情合理这也是逆变非常典型的应用。