泛型的高级特性
1、泛型实化
2、泛型协变
3、泛型逆变
-
泛型实化
在Java中(JDK1.5之后),泛型功能是通过泛型擦除来实现的。什么意思呢? 就是在对泛型的约束只是在编译阶段,运行的时候的JVM是识别不出来在代码中指定的类型的。 比如说List<String>,编译阶段限制了就是String,但在运行的时候JVM并不知道它本身只打算包含 “哪种类型”,只能识别它是个List。
Kotlin也是这样,然而不同的是Kotlin提供了内联函数的概念。内联函数的意思就是,在编译的时候自动将内联函数修饰的代码替换到调用它的地方,这样的话就不会有泛型擦除了,因为代码在编译之后会使用实际的类型来替换内联函数中泛型声明。举个例子说明:
fun getO() {
getSome<String>()
}
inline fun <T> getSome() {
//dosomething with T
}
上面的调用,经过实际替换是:
fun getO() {
//dosomething with String
}
可以看出来O调用了一个带有泛型的内联函数,O调用了getSome。在代码编译后,O函数中的代码将可以获得泛型的实际类型,也就是注释里写的。这意味着在Kotlin中泛型是可以得到实化的。
基于上述,我们可以思考一下如何获取对应的实化类型,举例:
fun getO() {
val type1 = getenericType<String>()
val type2 = getenericType<Int>()
println("result:$type1")
println("result:$type2")
}
inline fun <reified T> getenericType() = T::class.java
log->
result:class java.lang.String
result:class java.lang.Integer
可以看到讲泛型类型定义String,就能获取到String的类型,这就是泛型的实化。
reified这个函数,是我在这样写了之后,编译必须要加的,否则不通过。我又查阅了一些资料, 由此得到结论,获取泛型的实化必须被reified和inline
同时修饰。
结论
泛型的实化的前置条件 : 必须被reified和inline
同时修饰。
既然如此我自然想在项目中应用,举例:
inline fun <reified T> startActivity(context: Context?) {
val intent = Intent(context, T::class.java)
context?.startActivity(intent)
}
inline fun <reified T> startActivity(context: Context?, block: Intent.() -> Unit) {
val intent = Intent(context, T::class.java)
intent.block()
context?.startActivity(intent)
}
override fun onClick(v: View?) {
when (v) {
llOfflineMap -> {
startActivity<OfflineMapActivity>(activity)
}
llAboutUs -> {
startActivity<AboutActivity>(activity) {
putExtra("name", "name")
putExtra("name2", "name2")
}
}
}
}
-
泛型协变
假设某个方法接收的是Persion类型,但是你传入的是个Student类型。在Java中是不允许这么做,因为Student类型不能成为Persion类型,否则可能存在类型转换的安全隐患。说白了,程序会报 :类转换异常的错误。
而在Kotlin中,假设MyClass<T>的泛型类,其中A是B的子类型,同时MyClass<A>是MyClass<B>的子类型,那么我们就可以称MyClass在T这个泛型上是“协变”的。
如果一个泛型类在其泛型类型的数据上是只读的话,那么它是没有类型转换安全隐患的,而要实现这一点,则需要让MyClass<T>类中所有的方法都不能接收T类型的参数,换句话说就是。T只能出现在out位置上,而不能出现在in位置上。举例代码:
open class Persion(val name: String, val age: String)
class Student(val sname: String, val sage: String) : Persion(sname, sage)
student是Persion的子类。
class SimpleData<out T>(val aa: T?) {
fun getData(): T? {
return aa
}
}
SimpleData的泛型out T是只读,而参数aa是传入的T,不对其具有修改。那么:
fun main() {
val student = Student("name", "age")
val simpleData = SimpleData<Student>(student)
handleSimpleData(simpleData)
val data = simpleData.getData()
println("type:$data")
}
fun handleSimpleData(data: SimpleData<Persion>) {
//这里获取的是一个Persion类型,
//但是因为Persion是Student的父类,向上转型是完全安全的。
//如果某个方法接收的是List<Persion>类型,而传入的是List<Student>类型,在Java中是不允许这么做的。
val data1 = data.getData()
println("type:$data1")
}
这就是泛型的协变。
其实Kotlin已经默认给许多内置的API加上了协变声明。其实就包括了各种集合。Kotlin中的List本身就是只读的,如果你想要修改的话,需要使用MutableList才行!!!也就意味着它天然就是可以协变的。
List部份源码:
public interface List<out E> : Collection<E> {
// Query Operations
override val size: Int
override fun isEmpty(): Boolean
override fun contains(element: @UnsafeVariance E): Boolean
override fun iterator(): Iterator<E>
// Bulk Operations
override fun containsAll(elements: Collection<@UnsafeVariance E>): Boolean
}
可以看到List具有协变。
contains这个参数,可以看到E出现在了in位置上,实际contains只是为了判断当前集合中是否包含从参数中传入的这个元素,不会修改当前集合中的内容,因此这种操作实质上又是安全的。 但编译器不支持,为了让编译器通过加了@UnsafeVariance 注解。也就是说你想让它在in上,加@UnsafeVariance 注解编译器就不管,但这滥用这种功能导致的类型转换异常,要自己承担咯~
结论:
1、协变必须满足 接收参数与传入参数之间是父子关系。
2、协变的T必须是out位,如果想在in加@UnsafeVariance,但这种也是不涉及修改内容的。
-
泛型逆变
依据刚才的协变来说,直观的角度就是 ,MyClass<A>是MyClass<B>的子类型。逆变就是将此处返过来。假设MyClass<T>的泛型类,其中A是B的子类型,同时MyClass<B>是MyClass<A>的子类型,那么我们就可以称MyClass在T这个泛型上是“协变”的。
那么已知A是B的子类型,怎么让MyClass<B>过来成为MyClass<A>的子类型呢。举例如下:
open class Persion(val name: String, val age: String)
class Student(val sname: String, val sage: String) : Persion(sname, sage)
class Teacher(val tname: String, val tage: String) : Persion(tname, tage)
定义AB类型
interface TransForms<in T> {
fun transForms(t: T): String
}
fun main() {
val oo = (object : TransForms<Persion> {
override fun transForms(t: Persion): String {
return "value: ${t.name}" + "${t.age}"
}
})
setValue(oo)
}
fun setValue(trans: TransForms<Student>) {
val student = Student("zhangsan", "12")
trans.transForms(student)
}
从代码的角度来说,Student是Persion的子类。而逆变就在接口里:in
在泛型T声明上加了个in,意味着T现在只能出现在in的位置上,而不能出现在out的位置上。逆变的用法大概就这样了。
让我们继续深入,为什么逆变的时候泛型T不能出现在out上呢?我们假设它可以。
interface TransForms<in T> {
fun transForms(name: String, age: String): @UnsafeVariance T
}
为了让编译通过,@UnsafeVariance修饰。
fun main() {
val oo = (object : TransForms<Persion> {
override fun transForms(name: String, age: String): Persion {
return Teacher(name, age)
}
})
setValue(oo)
}
fun setValue(trans: TransForms<Student>) {
//想要student的返回值
val transForms = trans.transForms("zhangsan", "12")
}
上述代码就是典型的违反逆变规则而造成类型转换异常的例子。
在setValue方法中,我们期望得到的是一个Student的对象的返回,然而实际上
transForms方法返回的是一个Teacher对象。因此这里会造成类型转换异常的错误。
Exception in thread "main" java.lang.ClassCastException: com.hdsx.guangxihighway.Teacher cannot be cast to com.hdsx.guangxihighway.Student
at com.hdsx.guangxihighway.TestKt.setValue(Test.kt:20)
at com.hdsx.guangxihighway.TestKt.main(Test.kt:15)
at com.hdsx.guangxihighway.TestKt.main(Test.kt)
也就是说Kotlin在提供协变和逆变的时候,就已经把各种潜在的类转换安全隐患考虑进去了,只要严格按照其语法规则。就不会存在类型转换异常的情况。虽然@UnsafeVariance可以打破,但是也承担着额外的风险。
逆变的典型例子:
public interface Comparable<in T> {
/**
* Compares this object with the specified object for order. Returns zero if this object is equal
* to the specified [other] object, a negative number if it's less than [other], or a positive number
* if it's greater than [other].
*/
public operator fun compareTo(other: T): Int
}
为什么这个泛型上是逆变的呢,因为compareTo方法用于实现具体的比较逻辑。如果我们使用Comparable<Persion>实现了让两个Person对象比较大小的逻辑。那么用这段逻辑去比较两个Student对象的大小也一定是成立的。因此让Comparable<Persion>成为Comparable<Student>的子类型合情合理。
这就是逆变。