3.4 集合
本节介绍Kotlin中的List、Set、Map集合及集合的操作,包括添加元素、查找元素、过滤元素、转换等。
3.4.1 集合概述
Kotlin标准库提供了一整套用于管理集合的工具,集合通常包含相同类型的一些(数目也可以为零)对象。集合中的对象称为元素或条目。以下是Kotlin相关的集合类型。
List是一个有序集合,可通过索引(反映元素位置的整数)访问元素。元素可以在List中出现多次。List有一组字,这些字的顺序很重要并且字可以重复。
Set是唯一元素的集合。它反映了集合的数学抽象:一组不重复的对象。一般来说,Set中元素的顺序并不重要。
Map(或者字典)是一组键值对。键是唯一的,每个键都刚好映射到一个值,值可以重复。Map对存储对象之间的逻辑连接非常有用。
只读集合类型是型变的。这意味着,如果类Rectangle继承自Shape,则可以在需要List <Shape>的任何地方使用List<Rectangle>。换句话说,集合类型与元素类型具有相同的类继承关系。Map在值(value)类型上是型变的,但在键(key)类型上不是。反之,可变集合不是型变的;否则将导致运行时故障。
如图3.1所示的是Kotlin集合和接口的继承关系。
图3.1 Kotlin集合和接口的继承关系
Collection<T>是集合层次结构的根。此接口表示一个只读集合的共同行为:检索大小、检测是否为成员等。Collection继承自Iterable<T>接口,它定义了迭代元素的操作。可以使用Collection作为适用于不同集合类型的函数的参数。MutableCollection是一个具有写操作的Collection接口,例如add及remove。
List<T>以指定的顺序存储元素,并提供使用索引访问元素的方法。索引从0开始,直到最后一个元素的索引,即(list.size-1)。List中的元素(包括空值)可以重复:List可以包含任意数量的相同对象或单个对象。如果两个List在相同的位置具有相同大小和相同结构的元素,则认为它们是相等的。MutableList是可以进行写操作的List,例如,用于在特定位置添加或删除元素。在某些方面,List与数组(Array)非常相似。但是,它们有一个重要的区别—数组的大小是在初始化时定义的,永远不会改变;而List未预定义大小。作为写操作的结果,可以更改List的大小—添加、更新或删除元素。
Set<T>存储唯一的元素;元素的顺序通常未定义。null元素也是唯一的,一个Set中只能包含一个null。当两个Set具有相同的大小并且一个Set中的每个元素都能在另一个Set中存在相同元素,则两个Set相等。MutableSet是一个带有来自MutableCollection的写操作接口的Set。Set的默认实现是LinkedHashSet,保持元素插入时的顺序。另一种实现方式是HashSet,不声明元素的顺序。
Map<K, V>不是Collection接口的继承者,但它也是Kotlin的一种集合类型。Map存储键值对(或条目);键是唯一的,但是不同的键可以与相同的值配对。Map接口提供特定的函数进行通过键访问值、搜索键和值等操作。无论键值对的顺序如何,包含相同键值对的两个Map是相等的。MutableMap是一个具有写操作的Map接口,可以使用该接口添加一个新的键值对或更新给定键的值。Map的默认实现是LinkedHashMap,迭代Map时保持元素插入时的顺序。反之,另一种实现方式是HashMap,不声明元素的顺序。
创建集合的最常用方法是使用标准库函数listOf<T>()、setOf<T>()、mutableListOf<T>()、mutableSetOf<T>()。如果以逗号分隔的集合元素列表作为参数,编译器会自动检测元素类型。创建空集合时,必须明确指定集合类型。
同样地,Map也有这样的函数—mapOf()与mutableMapOf()。映射的键和值作为Pair对象传递(通常使用中缀函数to创建)。可以创建可写Map并使用写入操作填充它。apply()函数在初始化时使用。
创建没有任何元素的集合的函数有emptyList()、emptySet()与emptyMap()。创建空集合时,应指定集合将包含的元素类型。对于List,有一个接收List的大小与初始化函数的构造函数,该初始化函数根据索引定义元素的值。
要创建与现有集合具有相同元素的集合,可以使用复制操作。标准库中的集合复制操作创建了具有相同元素引用的浅复制集合。因此,对集合元素所做的更改会反映在其所有副本中。但是toList()、toMutableList()、toSet()等创建了一个具有相同元素的新集合,如果在源集合中添加或删除元素,则不会影响副本。副本也可以独立于源集合进行更改。这些函数还可用于将集合转换为其他类型,例如,根据List构建Set,反之亦然。
Kotlin支持使用迭代器遍历集合中的元素,迭代器可以在不暴露集合内部结构的情况下,顺序遍历集合中的元素。迭代器适用于一个一个处理集合元素的场景。Set和List的iterator()函数提供了迭代器,迭代器初始指向集合的第一个元素,next()函数返回当前元素并指向下一个元素。迭代器可以遍历集合中的元素,但不能获取元素。如果需要再次遍历集合,需要重新创建一个迭代器。此外,用for或者forEach语句也可以遍历集合中的元素。
List有一个特殊的迭代器ListIterator,它支持双向遍历,后续遍历用hasPrevious()、previous(),可以用nextIndex()和previousIndex()获取元素下标。
可变集合提供MutableIterator,当遍历集合时,可以用remove移除元素。MutableListIterator可以修改、添加元素。
Kotlin可以使用rangeTo创建一连串数值,通常用..替代rangeTo。这些数值可以用for语句遍历。如果要进行后续遍历,可以用downTo。这些数值的间隔通常是1,也可以用step指定步长。如果不需要遍历最后一个元素,可以用until。可以用in、!in判断某个元素是否在区间内。
Kotlin提供序列类型Sequence<T>。当迭代过程包含多个步骤时,每一步执行完,会产生一个结果,下面的步骤会在这个结果的基础上执行。序列中的每一个元素都要依次执行操作步骤,迭代器对集合中的所有元素执行完一个操作过程后再执行下一个。序列可避免产生中间结果,可以提高整个处理链的性能。
通常可以用sequenceOf()构建序列;可以用asSequence()将List、Set转换为序列;可以用generateSequence()显示指定序列的第一个元素,函数返回null时序列停止增长;序列也可以用组块生成,函数包含Lambda表达式,包含yield()、yieldAll()函数。yield()的参数是一个元素,yieldAll()的参数可以是一个集合,也可以是一个序列,这个序列可以无限长。
序列支持无状态的操作,如filter、take、map、drop等。下面的例子展示了序列的处理过程,对每个元素都会执行filter、map、take方法,在找出4个元素后,不再遍历其余元素。
3.4.2 集合操作
Kotlin标准库提供了很多函数操作集合,如set、add、search、sorting、filtering、transformations等。
集合有如下操作:转换(transformations)、过滤(filtering)、加减(plus and minus operators)、分组(grouping)、获取部分集合(retrieving collection parts)、排序(ordering)、聚合操作(aggregate operations)。
集合操作不会改变集合本身,而是生成一个新的集合存放处理后的结果。此外,还可以指定一个可变对象存放处理后的结果。
Kotlin提供了很多集合转换的扩展操作,这些操作会产生新的集合。下面介绍几种转换操作:映射(mapping),用Lambda表达式对当前集合中的元素进行处理,生成一个新的集合,两个集合中元素的顺序一致。当产生null元素时,可以用mapNotNull()函数将其过滤掉。当转换映射时,可以用mapKeys处理key值,或者用mapValues处理value值。
双路合并(zip)操作,将两个集合中的元素合并为一个List,List中每个元素成对出现,是Pair类型的。如果两个集合大小不一样,zip操作取较小的集合长度。此外,在zip操作的基础上,还可以进行Lambda操作,最终返回的List元素集合就是由Lambda操作后的元素组成的。相反,使用unzip函数可以进行反向操作。
关联(association)操作,可以对集合中的元素进行处理,以生成一个映射。基本的关联函数是associateWith(),集合中的元素作为映射的key,转换后的元素作为映射的value。associateBy()函数将集合中的元素作为映射的value,转换后的元素作为映射的key。
打平(flattern)操作,flattern()函数将若干集合中的元素放在一个集合中;flatternMap()函数对若干集合进行映射操作,然后再打平。
字符串处理操作,可以用joinToString()函数将集合中的元素生成一个字符串,用joinTo()函数将生成的字符串拼接在另一个字符串后。
过滤操作在集合处理中使用广泛。Kotlin使用谓词函数实现过滤操作,使用Lambda表达式处理集合的元素,并返回一个布尔值。如果需要使用集合元素的下标,可以使用filterIndexed()函数。如果需要使用相反条件过滤,可以使用filterNot()函数。如果需要过滤类型,可以使用filterIsInstance<T>()函数。需要过滤空,可以使用filterNotNull()函数。
可以使用partition()函数对集合进行划分,一部分满足过滤条件,另一部分不满足过滤条件。可以使用any()(至少有一个)、none()(一个也没有)、all()(全部满足)来检验谓词函数。
集合还可以进行加减操作,运算符的左值是集合,右值可以是一个元素或者集合。
Kotlin还提供了分组操作。groupBy函数使用一个Lambda表达式,返回一个映射。映射的key是Lambda表达式返回的值,value是集合中的元素。
Kotlin提供了丰富的操作以获取集合的一部分。slice()函数根据给定下标返回集合中的元素。take()函数从第一个元素开始,截取给定个数的集合元素;如果截取个数大于集合长度,返回整个集合元素。drop()函数从第一个元素开始,丢弃给定个数的元素,然后返回其余元素。如果要使用谓词函数,可以用takeWhile()、takeLastWhile()、dropWhile()函数、dropLastWhile()。chunked()函数将集合分块,遍历集合元素,达到给定数量,生成一个List,直到最后一个元素。windowed()函数从第一个元素开始进行遍历,可以指定窗口大小。此外,windowed()函数可以指定步长step等参数。如果滑动窗口中只有两个元素,可以用zipWithNext()函数。
如果要获取集合中的单个元素,可以使用elementAt()按位置获取,因first()获取第一个元素,用last()获取最后一个元素,也可以在first()或者last()后用谓词函数按条件获取。用random()函数可随机获取元素,用contains()检测是否存在某个元素,用containsAll()检测是否存在多个元素,用isEmpty()、isNotEmpty()判断集合是否为空。
Kotlin可以用Comparable接口对自定义的类型进行排序,Kotlin自带的类型默认支持排序,数值型按数值大小排序,字符型按照字母顺序排序。此外,还可以用Comparator自定义顺序,compareBy()是Comparator的简单写法。自然顺序可以用sorted()和sortedDescending()进行升序或降序操作。自定义顺序可以用sortedBy()和sortedByDescending()进行升序或降序操作。倒序可以用reversed()、asReversed()。随机顺序可以用shuffled()。
集合的聚合操作函数有求最小值min()、最大值max()、平均值average()、求和sum()、计数count(),带有函数的求最大值/最小值为maxBy()/minBy(),Comparator对象的求最大值/最小值为maxWith()/minWith(),带有谓词函数的求和为sumBy(),返回Double类型的求和为sumDouble(),累加为fold()、reduce(),fold()有初始值,reduce()开始时用前两个元素作为参数。
集合可以通过add()添加单个元素,addAll()添加多个元素,remove()删除元素,retainAll()保留符合条件的元素,clear()清空集合,minusAsign()和minus()删除元素。
3.4.3 List、Set、Map相关操作
List是使用广泛的集合,List可以按索引取元素,getOrElse()函数若取不到元素就返回默认值,getOrNull()函数若取不到元素就返回null。对于有序List,可以用binarySearch()进行二分查找,此外,可以自定义排序规则。
Set提供了求交集intersect()、合并union()、剔除交集元素subtract()等操作。
Map提供了取键、值操作,过滤键、值操作,plus和minus操作,以及添加和更新操作。