Kotlin开发笔记:集合和逆变协变

Kotlin开发笔记:集合和逆变协变

在这里插入图片描述

Kotlin中的集合

基本的集合类型

Kotlin中的集合类型和Java差不多,不过有些在名称上可能有出入,下面是Kotlin中的一些基本集合类型:

类型介绍
Pair两个值的元组
Triple三个值的元组
Array经过索引的,固定大小的对象和基元集合
List有序的对象集合
Set无序的对象集合
Map键值对对象集合

Kotlin中的视图

在Kotlin引入了视图的概念,简而言之,不同的视图类型会赋予我们对操作集合的不同权限。Kotlin中有两种不同的视图:只读或不可变视图,以及读写或可变视图

比如对于List来说,有两种视图,分别是List和MutableList,前者提供只读视图,后者提供读写视图。当我们用List视图时将无法修改列表,而用MutableList就可以。

    var li = listOf(1,2,3) as MutableList
    li.add(5)

比如我们运行上述代码就会报错,因为listOf函数会产生List视图的集合,这将导致我们无法修改列表,如果我们要续写就需要产生读写视图的列表:

    var li = mutableListOf(1,2,3) 
    li.add(5)

不过本质上这两种视图都是对List的引用,比如说我们可以用List视图引用同一个ArrayList:

    val ar = arrayListOf(1,2,3,4)
    val li:List<Int> = ar
    val li1:MutableList<Int> = ar

不过List视图的引用将无法修改列表本身。

Kotlin中的一些技巧

使用listOf等函数快速创建集合

这个其实在上面给的例子里已经体现了,我们可以使用arrayListOf,listOf等函数快速创建出我们想要的集合而无需再用构造函数。

使用to和mapOf快速创建表

Kotlin中提供了一个to拓展函数,这个函数将生成一个Pair类型的对象,比如

val p1 = "age" to 18

将会创建一个First为"age",Second为18的Pair对象。而这个对象又可以用于mapOf函数。这样我们就可以快速创建一个map,比如:

val mMap = mapOf("age" to 18,"code" to 10086)

这样就创建了一个键值对为< String , Int >类型的map,其中to之前的为Key,之后的为Value。

同时获取索引和值

在Java中,如果我们想要同时获取一个List的索引和值的话可能需要遍历或者采取别的手段来达到这个目的,而在Kotlin中,我们可以用解构来实现这个目的:

fun main() {
    val li = listOf("jack","anderson")
    for((index,value) in li.withIndex()){
        println("index : $index, value:$value")
    }
}

withIndex将返回一个包含键值对的对象,我们将其解构出来就可以同时获得索引和值了。

创建有规律的数组

接下来介绍的是如何创建出一个有规律的数字,比如我们可以创建出一个物的倍数的数组:

fun main() {
    val li = Array(5){index -> index * 5}
    for(value in li){
        println(value)
    }
}

Array括号后面的5是元素个数,index下标是从0开始,我们可以打印出值:
在这里插入图片描述
成功创建了一个包含五的倍数的数组,利用这个技巧我们再加上Array内置的一些方法,就可以实现许多计算,比如我们想要计算从1到5的平方和的话就可以直接这样写:

fun main() {
    val li = Array(5){index -> (index+1) * (index+1)}.sum()
    println(li)
}

使用in

在Java中如果我们想要判断一个元素是否在一个集合中,一般会使用contains方法,不过在Kotlin中提供了in运算符实现了同样的效果:

fun main() {
    val li = Array(5){index -> index*5}
    println(0 in li)
}

实际上在迭代时我们会用到in运算符也是这样。

Kotlin中的逆变和协变

什么是逆变和协变

首先我们需要介绍逆变和协变的概念,协变和逆变都是术语,前者指能够使用比原始指定的派生类型的派生程度更大(更具体的)的类型,后者指能够使用比原始指定的派生类型的派生程度更小(不太具体的)的类型。

以我的理解,协变应该接近于extend,而逆变接近于super。

默认情况下,在Java中泛型强制实行类型不变性–也就是说,如果泛型函数期望一个参数类型T,则不允许替换基类型T或者派生类型T,类型必须是完全预期的类型

实际上,在Java中我也没有对通配符和一般的泛型T的区别和相同有什么很深的理解。我的理解是,通配符?代表不确定的类型,泛型类型T代表确定的类型

类型不变性

这里再介绍一下类型不变性,当一个方法接收到一个类型为T的对象(确定对象,不是泛型对象)时,我们可以传入为T类型或者是T的子类的对象。比如如果一个方法接收一个Animal类型的对象,那么身为Animal子类的Cat类型的对象也可以被传进去。

但是,如果这个方法接收的是一个泛型类型为T的对象,那么将不允许传递派生类型为T的泛型对象。比如,如果可以传递List< Animal >类型的对象,那么将不允许传入List< Cat >类型的对象,这和Java中的类型擦除有关。

书上的一个例子我觉得很形象,比如说我们创建一个Fruit类和两个继承它的类还有一个接收水果的方法:

open class Fruit
class Orange:Fruit()
class Banana:Fruit()

fun receiveFruits(fruits:Array<Fruit>){
    println("水果的数量是${fruits.size}")
}

这个方法可以接受泛型类型为Fruit的数组,如果我们传入Orange或者Banana会怎么样呢?
在这里插入图片描述
可以看到,编译器提示类型不匹配了。香蕉是从水果继承而来的,但是显然一篮子香蕉不是从一篮子水果继承而来的

不过一旦我们用list视图来操作,上述代码就不会报错了:
在这里插入图片描述
这是因为List视图只允许我们进行读而不允许我们进行写,这样是安全的。在Kotlin中,这个效果是由于List视图是out修饰的,我们将在后面的协变中介绍。
在这里插入图片描述

使用协变

上边介绍到了,一旦我们使用List视图,那么receiveFruits方法就可以被调用了,这正是由于使用了协变的原因。接下来我们创建一个方法来模拟协变的使用场景,比如我们想要把一个Fruit的Array复制到另一个Fruit的Array中:

fun copyFromTo(from:Array<Fruit>,to:Array<Fruit>){
    for(i in 0 until from.size){
        to[i] = from[i]
    }
}

这种情况下我们显然不能传入除Fruit类之外的泛型类,比如:
在这里插入图片描述
编译器是不会允许我们传入泛型类型为Banana的参数给from的,这个时候我们只需要修改一下这个函数,在传入的from参数处使用协变即可:

fun copyFromTo(from:Array<out Fruit>,to:Array<Fruit>){
    for(i in 0 until from.size){
        to[i] = from[i]
    }
}

在这里插入图片描述
这样编译器就不会报错了。要理解这个协变的含义我们可以从编译器为什么不让我们传入Banana类型的参数看。如果我们可以传入Banana类型的参数,我们就有可能对Banana执行一些Fruit层面的指令。

举个例子来说,大部分水果冲洗完成之后就可以直接食用了,但是香蕉的果皮较厚,我们就不能直接食用,在这之前还需要剥皮。身为子类的Banana🍌肯定是有其特殊之处的,不能用基类Fruit的一些操作直接用在Banana上。但是如果我们不对这个Banana进行操作的话,那么就不会有什么大问题了,这就是协变的含义。

这里对copyFromTo方法的from参数加上out参数后就说明我们不会对这个from参数进行任何方法的调用了,我们只是单单读取这个参数,这样编译器就允许我们传入Fruit的子类的泛型类型了,换言之,我们就实现了协变。这种在使用泛型类型时使用协变的行为称之为“使用点型变”。

使用逆变

与协变相对的就是逆变了,如果说协变是只读不写的话,那么逆变就是只写不读。实际上也确实是这样,使用逆变将允许我们在该参数上进行设置值的方法调用,而不允许读取的方法。

我们依旧以上面的copyFromTo方法为例,现在我们希望可以将任意Fruit或者Fruit子类的元素复制到Fruit或Fruit超类的集合中,比如说我们传一个Any类的参数:
在这里插入图片描述
显然由于类型不变性这样是行不通的,在这里我们再次对copyFromTo方法做修改,这次我们对to参数使用逆变:

fun copyFromTo(from:Array<out Fruit>,to:Array<in Fruit>){
    for(i in 0 until from.size){
        to[i] = from[i]
    }
}

在这里插入图片描述
这样编译器就允许我们这样调用了。

使用Where的参数类型约束

这部分内容说白了就是约束泛型类型的范围,比如说我们有一个方法需要传入一个泛型类,这个泛型类需要实现AutoCloseable接口,那么我们就可以这样写

fun <T:AutoCloseable> useAndClose(input:T)
{
    input.close()
}

实际上上面的和Java中的写法也差不多,不过如果是一个泛型需要实现多个接口的话就不能这么写了,需要我们用where参数进行约束:

fun <T> useAndClose(input:T)
    where T:AutoCloseable,
        T:Appendable
{
    input.append("haha")
    input.close()
}

where约束跟在参数列表后面,花括号前面。约束参数中用逗号分隔。

星投影

星投影用<*>定义参数类型,它是指定泛型只读类型和原始类型的Kotlin等效物,**简单来说,我们可以用星投影捕获泛型类型,但是我们只能对捕获的泛型类型进行读取而不能修改。**当你想表达对类型不太了解但有希望类型安全时,请使用星投影,星投影只允许读出而不允许写入,比如:

fun printValues(values:Array<*>){
    for(value in values){
        println(value)
    }
}

在这个方法中我们用星投影捕获了泛型类型,但是我们只能读取values值不能写入或者更改values值,实际上就相当于out T,但是写起来更简洁。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值