现代的软件一般比较复杂,程序语言中的基本数据类型往往不能满足需要,除了基本的数据类型以外,还有对象的容器也非常的重要,比如线性容器(数组,列表和Set)和二维容器(哈希表)等。今天就来学习一下Kotlin中的容器。
集合就是用于处理一组对象的容器,因为用的人较多,所以就成了标准库。常见的集合有三种主要类型,列表类,Set类和Map类。
这里并不单纯指类List,主要的意思是线性的容器,它的特点是以相对顺序存储同一类型的对象,有一个整数索引(index)来表示其相对的位置,查找性能差,其他还好。代表为数组。
最简单也是使用最为广泛的线性容器,不用过多的介绍了,可以参考之前的文章。
最大的弊端就是长度是固定的,长度在创建数组时就确定了,后面就改不了了。所以,必须在事先要能够确定数组的长度。
比如数组的元素是一个Collection,而非常见的基本数据类型,这时要如何写?
val carray = arrayOf<MutableList<Int>>(
mutableListOf(),
mutableListOf()
)
val narray = Array<MutableList<Int>>(10) { mutableListOf() }
关键就在于要声明元素的类型,其他的与基本数据类型的数组是一样的。另外,如果数组数量比较少,方便直接写,那就用字面构造函数,其实很方便。或者用数组元素的构造方法也可以。
以最为常见的二维数组来说,要如何创建?
val smatrix = arrayOf(
arrayOf(1, 2, 3),
arrayOf(4, 5, 6),
arrayOf(7, 8, 9)
)
val matrix = Array(5) { IntArray(6) }
用于表示区间的表达式,最为直观理解就是数组的索引,用操作符…来表示区间,比如0~9,就是0…9,通常用于for-loop中:
if (i in 1..4) { // equivalent of i >= 1 && i <= 4
print(i)
}
for (i in 1..4) print(i) // for (int i = 1; i <= 4; i++) print(i)
还可以指定步长和边界,以及方向:
for (i in 0 until 10) { // for (int i = 0; i < 10; i++)
print(ln)
}
for (i in 0 until 10 step 2) { // for (int i = 0; i < 10; i += 2)
print(ln)
}
for (i in 9 downTo 0) { // for (int i = 9; i >= 0; i--)
print(i)
}
还可以用于字符,比如:
for (c in 'a'..'z') { // for (char c = 'a'; c <= 'z'; c++)
print(c)
}
Range是一个表达式,所以在其之上做其他操作,但需要注意这时需要加上括号,比如:
for (i in (0..9).filter {it % 2 == 0 }) {
println(i) // only evens
}
for (c in ('a'..'z').map { it.toUpperCase() }) {
println(c) // upper case
}
需要注意,虽然Ranges方便操作数组的索引,但如果想要带着索引遍历数组的话,还是要用专用的遍历方式,而不是用Range,比如:
for ((index, value) in array.withIndex()) {
println("the element: [$index] = $value")
}
Ranges是一个数据结构代表着一个区间,这个区间可能是一个整数范围,也可能是一个字符范围,其实也可以是其他自定义数据类型,只要能表达 出区间的概念。只不过整数区间是为常用的一种方式,以及整数区间可以方便当作数组和列表的索引。
但有时如果仅仅想重复一件事情n次,那就没有必要用Ranges,虽然它也可以,这时最为方便的是函数repeat,它与区间的唯一区别是repeat是没有返回值的,它仅是把一件事情重复n次,但没有返回值也就是说没有办法再转化为其他数组或者列表。
repeat(10) { println("repeat # $it") }
//repeat # 0
//repeat # 1
//repeat # 2
//repeat # 3
//repeat # 4
//repeat # 5
//repeat # 6
//repeat # 7
//repeat # 8
//repeat # 9
而比如Ranges是可以转化为其他数组和列表的:
(0 until 5).map { it * it }.toIntArray()
// [0, 1, 4, 9, 16]
列表可以简单理解为无限长的数组,它最大的特点是长度不固定,不必事先定好长度,它会随着添加元素而自动增长。所以,当你事先不知道容器的长度时,就需要用List。它是一个泛型,其余操作与数组一样。
val names = listOf("James", "Donald", "Kevin", "George")
names.map { it.toUpper() }
.forEach { println(it) }
序列与列表比较难区分,直观上它们是一样的。简单来说它并不是容器,它并不持有对象,它生产对象,类似于物理上的信号发射器和RxJava中的Observable,是有时序上的概念的,当你需要时它就生产出来一个元素。
队列可以用双端队列deque(读作dek),具体实现对象是ArrayDeque
双端队列是强大的数据结构,即可以用作队列,也可以用作栈。
Set是一个不含有重复元素的容器,特点是不会保存相对顺序,可以快速实现检索。
val names = setOf("James", "Harden", "Donald", "Joe")
for (nm in names) {
println(nm)
}
names.filter { it.length > 4 }
.forEach { println(it) }
由映射键->值对组成的二维容器,键不可重复,值可以重复,不会保存相对顺序,也可以用于快速检索。
val nameMap = mapOf("James" to 15, "Harden" to 30, "Donald" to 80, "Joe" to 86)
for (nm in nameMap.keys) {
println(nm)
}
for (age in nameMap.values) {
println(age)
}
for (e in nameMap.entries) {
println("${e.key} is ${e.value}")
}
nameMap.filter { it.key.length > 5 }
.forEach { println("${it.key} = ${it.value}") }
有一个地方需要特别注意,那就是容器的不可变性Immutability,用常规的方法创建的集合对象是不可变的Immutable,就是无法向其中添加元素也无法删除元素。对象的不可变Immutable在函数式编程中是很重要的特性可以有效的减少异步和并发带来的状态一致性问题。
val names = listOf("James", "Donald", "Kevin", "George")
names.add("Paul") // compile error, names is immutable
names.map { it.toUpper() }
.forEach { println(it) }
这样写会有编译错误,因为用listOf创建的列表对象是不可变的Immutable。如果想要改变就必须用支持更改的对象,如MutableList, MutableSet和MutableMap,如:
val names = mutableListOf("James", "Donald", "Kevin", "George")
names.add("Paul") // okay
names.map { it.toUpper() }
.forEach { println(it) }
如果有可能还是要尽可能的用不可变对象(Immutable objects)。
集合的操作就是函数式的三板斧过滤filter,转化map和折叠化约fold/reduce,前面讲的所有的容器都是支持的,结合lambdas可以写出非常规范的函数式代码。