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 MutableListli.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> = arval 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,但是写起来更简洁。