集合操作概述
Kotlin 标准库提供了用于对集合执行操作的多种函数。这包括简单的操作,例如获取或添加元素,以及更复杂的操作,包括搜索、排序、过滤、转换等。
扩展与成员函数
集合操作在标准库中以两种方式声明:集合接口的成员函数
和扩展函数
。
成员函数定义了对于集合类型是必不可少的操作。
例如,Collection
包含函数 isEmpty()
来检查其是否为空; List
包含用于对元素进行索引访问的 get()
,等等。
创建自己的集合接口实现时,必须实现其成员函数。
为了使新实现的创建更加容易,请使用标准库中集合接口的框架实现:AbstractCollection
、AbstractList
、AbstractSet
、AbstractMap
及其相应可变抽象类。
其他集合操作被声明为扩展函数。这些是过滤、转换、排序和其他集合处理功能。
公共操作
公共操作可用于只读集合
与可变集合
。 常见操作分为以下几类:
- 集合转换
- 集合过滤
- plus与minus操作符
- 分组
- 取集合的一部分
- 取单个元素
- 集合排序
- 集合聚合操作
这些页面中描述的操作将返回其结果,而不会影响原始集合。例如,一个过滤操作产生一个新集合,其中包含与过滤谓词匹配的所有元素。 此类操作的结果应存储在变量中,或以其他方式使用,例如,传到其他函数中。
val numbers = listOf("one", "two", "three", "four")
numbers.filter { it.length > 3 } // `numbers` 没有任何改变,结果丢失
println("numbers are still $numbers")
val longerThan3 = numbers.filter { it.length > 3 } // 结果存储在 `longerThan3` 中
println("numbers longer than 3 chars are $longerThan3")
对于某些集合操作,有一个选项可以指定 目标 对象。 目标是一个可变集合,该函数将其结果项附加到该可变对象中,而不是在新对象中返回它们。
对于执行带有目标的操作,有单独的函数,其名称中带有 To
后缀,例如,用 filterTo()
代替 filter()
以及用 associateTo()
代替 associate()
。
这些函数将目标集合作为附加参数。
val numbers = listOf("one", "two", "three", "four")
val filterResults = mutableListOf<String>() // 目标对象
numbers.filterTo(filterResults) { it.length > 3 }
numbers.filterIndexedTo(filterResults) { index, _ -> index == 0 }
println(filterResults) // 包含两个操作的结果
为了方便起见,这些函数将目标集合返回了,因此您可以在函数调用的相应参数中直接创建它:
// 将数字直接过滤到新的哈希集中,
// 从而消除结果中的重复项
val result = numbers.mapTo(HashSet()) { it.length }
println("distinct item lengths are $result")
具有目标的函数可用于过滤、关联、分组、展平以及其他操作。 有关目标操作的完整列表,请参见 Kotlin collections reference。
写操作
对于可变集合,还存在可更改集合状态的 写操作
。这些操作包括添加
、删除
和更新
元素。写操作在集合写操作以及 List
写操作与 Map
写操作的相应部分中列出。
对于某些操作,有成对的函数可以执行相同的操作:一个函数就地应用该操作,另一个函数将结果作为单独的集合返回。
例如, sort()
就地对可变集合进行排序,因此其状态发生了变化;
sorted()
创建一个新集合,该集合包含按排序顺序相同的元素。
val numbers = mutableListOf("one", "two", "three", "four")
val sortedNumbers = numbers.sorted()
println(numbers == sortedNumbers) // false
numbers.sort()
println(numbers == sortedNumbers) // true
集合转换
Kotlin 标准库为集合 转换 提供了一组扩展函数。 这些函数根据提供的转换规则从现有集合中构建新集合。 在此页面中,我们将概述可用的集合转换函数。
映射
映射 转换从另一个集合的元素上的函数结果创建一个集合。 基本的映射函数是 map()。 它将给定的 lambda 函数应用于每个后续元素,并返回 lambda 结果列表。 结果的顺序与元素的原始顺序相同。
如需应用还要用到元素索引作为参数的转换,请使用 mapIndexed()
。
val numbers = setOf(1, 2, 3)
println(numbers.map { it * 3 })
println(numbers.mapIndexed { idx, value -> value * idx })
如果转换在某些元素上产生 null 值,则可以通过调用 mapNotNull()
函数取代 map()
或 mapIndexedNotNull()
取代 mapIndexed()
来从结果集中过滤掉 null
值。
val numbers = setOf(1, 2, 3)
println(numbers.mapNotNull { if ( it == 2) null else it * 3 })
println(numbers.mapIndexedNotNull { idx, value -> if (idx == 0) null else value * idx })
映射转换时,有两个选择:转换键,使值保持不变,反之亦然。
要将指定转换应用于键,请使用 mapKeys()
;
反过来,mapValues()
转换值。 这两个函数都使用将映射条目作为参数的转换,因此可以操作其键与值。
val numbersMap = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key11" to 11)
println(numbersMap.mapKeys { it.key.toUpperCase() })
println(numbersMap.mapValues { it.value + it.key.length })
合拢
合拢 转换是根据两个集合中具有相同位置的元素构建配对。
在 Kotlin 标准库中,这是通过 zip()
扩展函数完成的。 在一个集合(或数组)上以另一个集合(或数组)作为参数调用时,zip()
返回 Pair
对象的列表(List
)。 接收者集合的元素是这些配对中的第一个元素。 如果集合的大小不同,则 zip()
的结果为较小集合的大小;结果中不包含较大集合的后续元素。
zip()
也可以中缀形式调用 a zip b
。
val colors = listOf("red", "brown", "grey")
val animals = listOf("fox", "bear", "wolf")
println(colors zip animals)
val twoAnimals = listOf("fox", "bear")
println(colors.zip(twoAnimals))
也可以使用带有两个参数的转换函数来调用 zip():接收者元素和参数元素。 在这种情况下,结果 List 包含在具有相同位置的接收者对和参数元素对上调用的转换函数的返回值。
val colors = listOf("red", "brown", "grey")
val animals = listOf("fox", "bear", "wolf")
println(colors.zip(animals) { color, animal -> "The ${animal.capitalize()} is $color"})
当拥有 Pair
的 List
时,可以进行反向转换 unzipping
——从这些键值对中构建两个列表:
- 第一个列表包含原始列表中每个 Pair 的键。
- 第二个列表包含原始列表中每个 Pair 的值。
要分割键值对列表,请调用 unzip()
。
val numberPairs = listOf("one" to 1, "two" to 2, "three" to 3, "four" to 4)
println(numberPairs.unzip())
关联
关联 转换允许从集合元素和与其关联的某些值构建 Map。 在不同的关联类型中,元素可以是关联 Map 中的键或值。
基本的关联函数 associateWith()
创建一个 Map
,其中原始集合的元素是键,并通过给定的转换函数从中产生值。 如果两个元素相等,则仅最后一个保留在 Map
中。
val numbers = listOf("one", "two", "three", "four")
println(numbers.associateWith { it.length })
为了使用集合元素作为值来构建 Map
,有一个函数 associateBy()
。
它需要一个函数,该函数根据元素的值返回键。
如果两个元素相等,则仅最后一个保留在 Map
中。
还可以使用值转换函数来调用 associateBy()
。
val numbers = listOf("one", "two", "three", "four")
println(numbers.associateBy { it.first().toUpperCase() })
println(numbers.associateBy(keySelector = { it.first().toUpperCase() }, valueTransform = { it.length }))
另一种构建 Map
的方法是使用函数 associate()
,其中 Map
键和值都是通过集合元素生成的。
它需要一个 lambda 函数,该函数返回 Pair:键
和相应 Map
条目的值。
请注意,associate()
会生成临时的 Pair
对象,这可能会影响性能。
因此,当性能不是很关键或比其他选项更可取时,应使用 associate()
。
后者的一个示例:从一个元素一起生成键和相应的值。
val names = listOf("Alice Adams", "Brian Brown", "Clara Campbell")
println(names.associate { name -> parseFullName(name).let { it.lastName to it.firstName } })
此时,首先在一个元素上调用一个转换函数,然后根据该函数结果的属性建立 Pair。
打平
如需操作嵌套的集合,则可能会发现提供对嵌套集合元素进行打平访问的标准库函数很有用。
第一个函数为 flatten()
。
可以在一个集合的集合(例如,一个 Set
组成的 List
)上调用它。
该函数返回嵌套集合中的所有元素的一个 List
。
val numberSets = listOf(setOf(1, 2, 3), setOf(4, 5, 6), setOf(1, 2))
println(numberSets.flatten())
另一个函数——flatMap()
提供了一种灵活的方式来处理嵌套的集合。
它需要一个函数将一个集合元素映射到另一个集合。 因此,flatMap()
返回单个列表其中包含所有元素的值。
所以,flatMap()
表现为 map()
(以集合作为映射结果)与 flatten()
的连续调用。
val containers = listOf(
StringContainer(listOf("one", "two", "three")),
StringContainer(listOf("four", "five", "six")),
StringContainer(listOf("seven", "eight"))
)
println(containers.flatMap { it.values })
字符串表示
如果需要以可读格式检索集合内容,请使用将集合转换为字符串的函数:joinToString()
与 joinTo()
。
joinToString()
根据提供的参数从集合元素构建单个 String
。 joinTo()
执行相同的操作,但将结果附加到给定的 Appendable
对象。
当使用默认参数调用时,函数返回的结果类似于在集合上调用 toString()
:各元素的字符串表示形式以空格分隔而成的 String
。
val numbers = listOf("one", "two", "three", "four")
println(numbers)
println(numbers.joinToString())
val listString = StringBuffer("The list of numbers: ")
numbers.joinTo(listString)
println(listString)
要构建自定义字符串表示形式,可以在函数参数 separator
、prefix
与 postfix
中指定其参数。
结果字符串将以 prefix
开头,以 postfix
结尾。除最后一个元素外,separator
将位于每个元素之后。
val numbers = listOf("one", "two", "three", "four")
println(numbers.joinToString(separator = " | ", prefix = "start: ", postfix = ": end"))
对于较大的集合,可能需要指定 limit
——将包含在结果中元素的数量。 如果集合大小超出 limit
,所有其他元素将被 truncated
参数的单个值替换。
val numbers = (1..100).toList()
println(numbers.joinToString(limit = 10, truncated = "<...>"))
最后,要自定义元素本身的表示形式,请提供 transform
函数。
val numbers = listOf("one", "two", "three", "four")
println(numbers.joinToString { "Element: ${it.toUpperCase()}"})
过滤
过滤是最常用的集合处理任务之一。在Kotlin中,过滤条件由 谓词 定义——接受一个集合元素并且返回布尔值的 lambda 表达式:true
说明给定元素与谓词匹配,false
则表示不匹配。
标准库包含了一组让你能够通过单个调用就可以过滤集合的扩展函数。这些函数不会改变原始集合,因此它们既可用于可变集合也可用于只读集合。为了操作过滤结果,应该在过滤后将其赋值给变量或链接其他函数。
按谓词过滤
基本的过滤函数是 filter()
。当使用一个谓词来调用时,filter()
返回与其匹配的集合元素。对于 List
和 Set
,过滤结果都是一个 List
,对 Map
来说结果还是一个 Map
。
val numbers = listOf("one", "two", "three", "four")
val longerThan3 = numbers.filter { it.length > 3 }
println(longerThan3)
val numbersMap = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key11" to 11)
val filteredMap = numbersMap.filter { (key, value) -> key.endsWith("1") && value > 10}
println(filteredMap)
filter()
中的谓词只能检查元素的值。如果想在过滤中使用元素在集合中的位置,应该使用 filterIndexed()
。
它接受一个带有两个参数的谓词:元素的索引和元素的值。
如果想使用否定条件来过滤集合,请使用 filterNot()
。它返回一个让谓词产生 false
的元素列表。
val numbers = listOf("one", "two", "three", "four")
val filteredIdx = numbers.filterIndexed { index, s -> (index != 0) && (s.length < 5) }
val filteredNot = numbers.filterNot { it.length <= 3 }
println(filteredIdx)
println(filteredNot)
还有一些函数能够通过过滤给定类型的元素来缩小元素的类型:
filterIsInstance()
返回给定类型的集合元素。
在一个 List<Any>
上被调用时,filterIsInstance<T>()
返回一个 List<T>
,从而让你能够在集合元素上调用 T
类型的函数。
val numbers = listOf(null, 1, "two", 3.0, "four")
println("All String elements in upper case:")
numbers.filterIsInstance<String>().forEach {
println(it.toUpperCase())
}
filterNotNull()
返回所有的非空元素。
在一个 List<T?>
上被调用时,filterNotNull()
返回一个 List<T: Any>
,从而让你能够将所有元素视为非空对象。
val numbers = listOf(null, "one", "two", null)
numbers.filterNotNull().forEach {
println(it.length) // 对可空的 String 来说长度不可用
}
划分
另一个过滤函数 – partition() – 通过一个谓词过滤集合并且将不匹配的元素存放在一个单独的列表中。因此,你得到一个 List 的 Pair 作为返回值:第一个列表包含与谓词匹配的元素并且第二个列表包含原始集合中的所有其他元素。
val numbers = listOf("one", "two", "three", "four")
val (match, rest) = numbers.partition { it.length > 3 }
println(match)
println(rest)
检验谓词
最后,有些函数只是针对集合元素简单地检测一个谓词:
- 如果至少有一个元素匹配给定谓词,那么
any()
返回true
。 - 如果没有元素与给定谓词匹配,那么
none()
返回true
。 - 如果所有元素都匹配给定谓词,那么
all()
返回true
。 注意,在一个空集合上使用任何有效的谓词去调用all()
都会返回true
。这种行为在逻辑上被称为 vacuous truth。
val numbers = listOf("one", "two", "three", "four")
println(numbers.any { it.endsWith("e") })
println(numbers.none { it.endsWith("a") })
println(numbers.all { it.endsWith("e") })
println(emptyList<Int>().all { it > 5 }) // vacuous truth
any()
和 none()
也可以不带谓词使用:在这种情况下它们只是用来检查集合是否为空。 如果集合中有元素,any()
返回 true
,否则返回 false
;none()
则相反。
val numbers = listOf("one", "two", "three", "four")
val empty = emptyList<String>()
println(numbers.any())
println(empty.any())
println(numbers.none())
println(empty.none())