Kotlin专题「十五」:泛型

前言:彪悍的人生,不需要解释,只要你按时达到目的地,很少有人在乎你开的是奔驰还是拖拉机。

一、概述

  在 Java 中,其实所谓的泛型就是类型的参数化。如果方法有入参,那么这些入参面前往往会有类型,这个类型就是修饰参数所用。假如我们在创建类型的时候,也为其指定了参数,这个参数又是个类型,这种我们就称为泛型。

本篇中会涉及到 Java 的泛型、PECS法则以及协变和逆变,有不了解的先看看Java 中的泛型以及协变和逆变(PECS法则),不然下面看着会懵逼哦。

Java 类型系统中最棘手的一部分就是通配符类型,Java 中为什么要支持这些通配符?使用有界通配符来增加API的灵活性。而 Kotlin 没有通配符这一说,相反,它有另外两个特征:声明站点变量和类型投影。

泛型简单使用

Kotlin中的类可能有参数类型 T:

    class People<T>(arg: T) {
        var name = arg
    }

通常,创建一个这样的实例,需要提供参数类型:

	var people: People<String> = People("Kotlin")

但是如果参数类型可能被推断出来,例如从构造函数参数或其他方法,我们可以忽略类型参数:

	var people = People("Kotlin")

二、声明处变量(Declaration-site variance)

要了解声明处变量,还的先回到 Java 泛型限制问题中。假如我们有一个泛型接口 MyList<T>,它没有任何以 T 作为参数的方法,只有返回 T 的方法:

	//Java
    interface MyList<T> {
        T shareT();
    }
    
    void invite(MyList<String> strs) {
        //MyList<Object> objects = strs;//报错,在Java中是不允许的
    }

将对 MyList<String> 实例的引用存储在 MyList<Object> 类型的变量中是非常安全的,因为没有消费者方法可以调用,但是 Java 不知道这一点,并且是禁止这样做的。

要解决这问题,使用通配符写法必须声明类型为 MyList<? extends Object>

    void invite(MyList<String> strs) {
        MyList<? extends Object> objects = strs;//java通配符写法
    }

因为子类通配符实际上是限制写入元素的,但是这里我们并没有写入任何元素(MyList 只有一个 shareT() 方法,但是编译器并不知道),按道理不使用子类通配符也能编译通过,然而 Java 却不允许编译通过,这就是 Java 泛型的一个弊端。

Kotlin 为了解决上面的问题,引入了声明点变量。可以向编译器解析这种情况,作用就是在泛型类型前面添加特定的修饰符,来保证只会返回特性元素(即PECS中的生产),而不会消费任何元素(即PECS中的消费)。

  • out :型变注释,使参数类型协变。由于它是在类型参数的声明侧(如 MyList< out T>)所以称为声明点变量,这与 Java 的协变< ? extends T>类似,在 Java 中,使用类型中的通配符使类型协变,适用于生产者场景;
  • in:型变注释,声明点变量,与 out 相反,使参数类型逆变。与 Java 的逆变<? super T>类似,它只能被消费,不能被生产,适用于消费者场景。

协变和逆变主要用来描述类型转换后的继承关系。

2.1 out (协变)

我们可以使用 out 修饰符修饰 MyList 接口中的参数类型 T ,以确保仅从 MyList<T> 的成员返回(产生)该参数,并且不会使用它:

    //T 使用 out 修饰符修饰
    interface MyList<out T> {
        fun shareT(): T
    }

    fun invite(strs: MyList<String>) {
        val any: MyList<Any> = strs//这是可以的,因为 T 是一个 out 类型参数
    }

使用规则:当声明类 MyList 的参数类型 T 被 out 声明时,它只能出现在 MyList 成员的外部,MyList<Base> 可以安全地成为 MyList<Derived> 的超类型。

接口 MyList 在参数 <out T> 修饰是协变的,或者 T 是协变类型参数,你可以将 MyList 视为 T 的生产者,而不是 T 的消费者。

什么是协变?

协变就是只要类型参数具有继承关系就认为整个泛型类型具有继承关系,比如:String 继承自 Any,那么就可以认为 MyList< String > 是 MyList< Any > 的子类型,于是 MyList< String > 类型变量赋值给 MyList< Any > 类型,这就是协变。

2.2 in(逆变)

除了 out 之外,Kotlin 还提供了互补的型变注释:in 。它是类型参数成为逆变,它只能被消费,不能被生产。可逆类型的一个很好的例子是 Comparable

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

    fun test(c: Comparable<Number>) {
        c.compareTo(1.0) //1.0是Double类型,它是Number的子类型
        val y: Comparable<Double> = c //compile error,进行写入操作,不允许
    }

error:(IntelliJ says)

Type mismatch. Required:Comparable Double; Found:Comparable Number>

可以看到最后一行报错了,依然是类型冲突。在 Java 如果需要得到解决则使用逆变<? super T>,那么Comparable<? super Double> y = c;才会成立,在 Kotlin 中逆变使用 in修饰:

    //T 使用 in 修饰符修饰
    interface Comparable<in T> {
        operator fun compareTo(other: T): Int
    }

    fun test(c: Comparable<Number>) {
        c.compareTo(1.0) //1.0是Double类型,它是Number的子类型
        //因此,我们可以将c分配给Comparable<Double>类型的变量
        val y: Comparable<Double> = c
    }

什么是逆变?

如果 String 继承自 Any,那么就可以认为 MyList< String > 是 MyList< Any > 的父类型,可以允许父类型变量赋值给子类型变量,比如上面的 Comparable< Number > 类型变量 c 赋值给 Comparable< Double > 类型变量 y,这就是协变。

PECS原则

kotlin中的声明点变量可以相对于 Java 中的 PECS 原则:可简称为CIPO

CIPO 全称为 Consumer-In-Producer-Out。

三、类型投影

3.1 使用站点差异:类型投影

将类型参数 T 声明为注释是非常方便的,可以避免声明处变量的子类问题,但有些类实际上不能被限制为只返回 T。我们来看看下面这个例子:

    class Array<T>(val size: Int) {
        fun get(index: Int): T {
            //TODO
        }

        fun set(index: Int, value: Int) {
            //TODO
        }
    }

这个类在 T 中不能是协变或逆变的,下面定义了一个copy 数组的方法,将 from 数组中的元素复制到 to 数组中:

    fun copy(from: Array<Any>, to: Array<Any>) {
        assert(from.size == to.size)
        for (i in from.indices)
            to[i] = from[i]
    }

copy() 函数是从一个数组复制 items 到另一个数组。来看看实际应用:

    val ints: Array<Int> = arrayOf(1, 2, 3)
    val anys = Array<Any>(3) { "" }

    //copy(ints, anys) //编译报错,类型为Array<Int>,但应为Array<Any>

遇到了同样的问题,Array<T>T 中是不变的,因此 Array <Int>Array <Any> 都不是另一个的子类型,为什么要限制?因为 Kotlin 认为有可能会对 from 数组写入操作,比如尝试将一个 String 写到 from: Array<Any> 数组中,如果我们实际上传入的是一个Int数组,这就会引起类型转换异常!所以 kotlin 对这种情形进行了限制。

然后,我们唯一想要确保的是 copy() 中不允许 from 进行写入操作,告诉编译器我们只想要读取 from ,那么可以这样做:

//这里将from声明为了<out Any> 泛型协变,类似 Java 的协变<? extends T>
//能接收Any或者Any子类类型,表示不可写,只可读。
fun copy(from: Array<out Any>, to: Array<Any>) {}

copy() 函数中的 from 的参数类型使用 out 关键字修饰,即泛型协变。目的就是可以使用读操作,而不使用写操作。这种情况叫类型投影。

from 是一个受限的(投影的)数组,只能调用那些返回参数类型 T 的方法,不能写入元素。在这种情况下,我们只能调用 get()。这是我们处理 使用-站点差异 的方法,它类似 Java 的协变 <? extends T>

	val ints: Array<Int> = arrayOf(1, 2, 3)
	val anys = Array<Any>(3) { "" }

	copy(ints, anys) // compile success,编译器已经知道from只可读不可写,所以允许我们这么传入。

当然,你也可以使用 in 投射类型:

	//dest使用了in修饰,表示可写,类似于java中的<? super T>
	//接收String类型及其超类型。
    fun fill(dest: Array<in String>, value: String) { 
            for (i in dest.indices)
            dest[i] = value
     }

	fun test(){
		//调用
		val chars: kotlin.Array<CharSequence> = arrayOf("1", "2", "3")
		fill(chars, "1") // compile success,CharSequence 是 String 的超类
	}

Array<in String> 数组对应 Java 的 Array<? super String> ,即你可以传递一个 CharSequence 或者对象数组到 fill() 函数。

3.2 星状投影

有些时候,我们并不知道类型参数到底是什么,但是我们依然想安全的使用这些类型参数,该怎么办?正式基于上面的考虑,kotlin为我们提供了星号映射,其修饰符为*。

Kotlin 为此提供了星状投影语法:

  • Foo< out T : TUpper>:  其中 T 是具有上限 TUpper 的协变型参数,Foo<*> 等效于 Foo<out TUpper>。这意味着当 T 未知时,你可以安全地从 Foo<*> 读取TUpper 的值;
  • Foo< in T >:  其中 T 是一个协变类型参数,Foo<*> 等效于 Foo<in Nothing>。这意味着当 T 未知时,你不能以安全的方式写任何东西给Foo<*>
  • Foo< T : TUpper >:  其中 T 是上限类型为 TUpper 的不变类型参数,Foo<*> 等效于Foo<out TUpper>读取值,以及 Foo<in Nothing> 写入值。

如果泛型类型有多个类型参数,则每个参数都可以独立投影。例如,如果将类型声明为接口 Function<in T, out U> ,我们可以想象以下星像投影:

  • Function<*, String>表示 Function<in Nothing, String>
  • Function<Int, *> 表示 Function<Int, out Any?>
  • Function<*, *> 表示 Function<in Nothing, out Any?>

注意:星型投影非常类似于Java 的原始类型,但是很安全。

    class Student<in T, out E>(t: T, val e: E) {}
    
    fun test() {
    	val student: Student<*, String> = Student(0, "Android")//*代替了in修饰的类型,表示In Nothing
	 	val student2: Student<Number, *> = Student(0, "Android")//*代替了out修饰的类型,表示out Any?
		val student3: Student<*, *> = Student(0, "Android")
    }

四、泛型函数

不仅类可以有类型参数,函数也可以有,类型参数放在函数名称的前面:

    fun <T> T.basic(): String {
        //TODO
    }

    fun <T> signleList(item: T): List<T> {
         //TODO
    }

要调用泛型函数,需要在函数名称后的调用站点上指定类型参数:

val list = signleList<Int>(0)

但是如果能从上下文推断出类型实参,那么可以将其省略:

val list = signleList(0)

五、泛型约束

可以用给定类型参数替代的所有可能类型的集合可能受到泛型约束的限制。

上限

最常见的约束是与 Java 的 extends 关键字相对应的上限。

    fun <T : Comparable<T>> sort(list: List<T>) {
        //TODO
    }

冒号 : 后面指定的类型是上限:只能将 Comparable<T> 的子类型替换为 T

    sort(listOf(1, 2, 3))//Int是Comparable<Int>的子类型
    //sort(listOf(HashMap<Int, String>()))//报错,HashMap<Int, String>()不是Comparable<HashMap<Int, String>>的子类型

默认上限(如果未指定)是 Any? 尖括号 <> 内只能指定一个上限,如果同一个类型参数需要两个或两个以上的上限,则需要单独的使用 where 语句。

    fun <T> copyWhenGreater(list: List<T>, threshold: T): List<String>
            where T : CharSequence, T : Comparable<T> {
        return list.filter { it == threshold }.map { it.toString() }
    }

传递的类型必须同时满足 where 语句的所有条件,在上面的例子中,T 类型必须同时实现CharSequence和 Comparable<T>

    var whenList = copyWhenGreater(listOf("一", "二", "三"), "三")
    Log.e(TAG, "whenList == $whenList")

String类型同时满足CharSequence和 Comparable<T>,打印数据如下:

GenericsActivity: whenList == []

六、类型擦除

Kotlin 针对泛型声明用法执行的类型安全检查仅仅在编译时进行。在运行时,泛型类型的实例不保存有关其实际类型参数的任何信息,类型信息被称为擦除。例如:Foo<Bar>Foo<Baz?> 的实例被擦除为 Foo<*>

因此,没用通用的方法来检查运行时是否使用某些类型参数创建了泛型类型的实例,并且编译器禁止这种 is-checks

类型转换为带有具体类型参数的泛型类型,例如:foo as List<String>,不能在运行时检查。

当高级程序逻辑隐含了类型安全,但编译器不能直接推断时,可以使用这些未检查类型强制转换。编译器会在未检查的强制类型转换上发出警告,并且在运行时,只检查非泛型部分(相当于foo的List<*>)。

泛型函数调用的类型参数也在编译时检查。在函数体内,不能使用类型函数进行类型检查,并且未选中类型强制转换为对类型参数(foo as T) 。但是,内联函数的具体化类型参数会被调用站点内联函数体中的实际类型参数所替代,因此可以用于类型检查和强制类型转换,对泛型类型的实例有如上所述的相同限制。

源码地址:https://github.com/FollowExcellence/KotlinDemo-master

点关注,不迷路


好了各位,以上就是这篇文章的全部内容了,能看到这里的人呀,都是人才

我是suming,感谢各位的支持和认可,您的点赞、评论、收藏【一键三连】就是我创作的最大动力,我们下篇文章见!

如果本篇博客有任何错误,请批评指教,不胜感激 !

要想成为一个优秀的安卓开发者,这里有必须要掌握的知识架构,一步一步朝着自己的梦想前进!Keep Moving!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值