Kotlin泛型的高级特性(六)

泛型的高级特性
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>的子类型合情合理。

这就是逆变。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,924评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,781评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,813评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,264评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,273评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,383评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,800评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,482评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,673评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,497评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,545评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,240评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,802评论 3 304
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,866评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,101评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,673评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,245评论 2 341