一文了解 Kotlin 中的集合操作函数

1,966 阅读8分钟

如下图所示,Kotlin 标准库提供了用于对集合执行操作的多种函数。可以看到光下面的共有函数就有 98个。我们可以它们分成转换、过滤、加减、分组、取集合一部分、获取集合单个元素、集合排序、集合聚合操作这几个部分,并分别来介绍它们的功能。

Kotlin集合操作.jpg

转换

屏幕截图 2024-05-04 211416.png

映射

image.png

映射转换的本质是:从集合A的元素上的函数结果创建一个新的集合B。如下图所示,映射操作相关的函数有 map、mapNotNull、mapIndexed、mapIndexedNotNull、mapKeys、mapValuse。

屏幕截图 2024-05-04 180022.png

对于 list、set、map公共的函数介绍如下:

函数作用
map将给定的 lambda 函数应用于每个后续元素,并返回 lambda 结果列表
mapIndexed和 map 一样,区别是参数有元素索引
mapNotNull和 map 一样,区别是会过滤 null
mapIndexedNotNull和 mapIndexed 一样,区别是会过滤 null

代码示例如下:

val numbers = listOf(1, 2, 3)
numbers.map { it * 3 } // [3, 6, 9]
numbers.mapIndexed { idx, value -> value * idx } // [0, 2, 6]
numbers.mapNotNull { if ( it == 2) null else it * 3 } // [3, 9]
numbers.mapIndexedNotNull { idx, value -> if (idx == 0) null else value * idx } // [2, 6]

对于 map,还可以选择只转换 key,或者 value。这时就需要用到 mapKeys()、mapValuse() 函数。

val numbersMap = mapOf("key1" to 1, "key2" to 2, "key3" to 3)
val mapKeys = numbersMap.mapKeys { it.key.toUpperCase() } // {KEY1=1, KEY2=2, KEY3=3}
val mapValues = numbersMap.mapValues { it.value + it.key.length } // {key1=5, key2=6, key3=7}

合拢

image.png

从图中可以看出,合拢转换的本质是将两个集合中具有相同位置的元素构建配对,默认是使用 Pair 存储两个集合的值,当然我们也可以自定义合拢转换逻辑。

如下图所示,合拢转换的函数只有 zip、unzip。zip 是合拢操作,而 unzip 是分割操作

屏幕截图 2024-05-04 182031.png

代码示例如下:

//zip合拢操作
val list1 = listOf(1, 2, 3)
val list2 = listOf('a', 'b', 'c')
list1.zip(list2) // [Pair(1, 'a'), Pair(2, 'b'), Pair(3, 'c')]
list1.zip(list2) { t,v -> "$v = $t"} // ["a = 1" , "b = 2", "c = 3"]

//unzip 分割操作
val numberPairs = listOf("one" to 1, "two" to 2, "three" to 3, "four" to 4)
numberPairs.unzip() // Pair([one, two, three, four], [1, 2, 3, 4])

关联

image.png

从图中可以看出,关联转换的本质是从集合元素和与其关联的某些值构建 Map。

其对应的函数有 associateWith、associateBy、associate,如下图所示:

屏幕截图 2024-05-04 184824.png

函数作用
associateWith创建一个 Map,集合值作为 key,函数生成的值作为 value。key 相同会保留最后一个
associateBy创建一个 Map,默认集合值作为 value,函数生成的值作为 key。可以自定义key-value的生成函数。key 相同会保留最后一个。
associate创建一个 Map,函数返回 Pair对象

代码示例如下:

// associateWith:集合值作为 key,函数生成的值作为 value。
val numbers = listOf("one", "two", "three", "four","four")
numbers.associateWith { it.length } // {one=3, two=3, three=5, four=4}

//associateBy:默认集合值作为 value,函数生成的值作为 key
numbers.associateBy { it.first().toUpperCase() } // {O=one, T=three, F=four}
//自定义key-value的生成函数,结果为 {O=3, T=5, F=4}
numbers.associateBy(
    keySelector = { it.first().toUpperCase() }, 
    valueTransform = { it.length }
)

//associate:返回 Pair对象,结果为 {O=E, T=E, F=R}
numbers.associate { it.first().toUpperCase() to it.last().toUpperCase() }

展平

image.png

如图所示,展平转换的本质是将嵌套集合元素展开。它对应的函数只有 flatten 和 flatMap 两个。flatten 函数可以在一个集合的集合上调用,并返回嵌套集合中的所有元素的一个List;flatMap 需要一个函数作为参数,这个参数函数将一个集合元素映射到另一个集合。 因此可以看作 flatMap = map + flatten() 的连续调用

屏幕截图 2024-05-04 190814.png

代码示例如下:

//使用flatten
val list = listOf(setOf(1, 2, 3), setOf(4, 5), setOf(6, 7))
list.flatten() // [1, 2, 3, 4, 5, 6, 7]

//使用flatMap
val map = mapOf(1 to 2, 3 to 4)  
map.flatMap({ listOf(it.key, it.value) } ) // [1, 2, 3, 4]

字符串表示

字符串表示就是将集合转化为字符串。kotlin 提供了两个函数,分别是 joinToString 与 joinTo。它们的区别是,joinTo 多一个参数,来将结果附加到给定的Appendable(可追加)对象上。代码示例如下:

//使用joinToString
val numbers = listOf("one", "two", "three", "four")
numbers.joinToString() // 返回一个String,内容为 one, two, three, four

//使用joinTo
//内容为 one, two, three, four
val stringBuilder = StringBuilder()
numbers.joinTo(stringBuilder)

//自定义样式
numbers.joinToString(separator = " | ", prefix = "start: ", postfix = ": end")
numbers.joinToString {
    "Element: ${it.toUpperCase()}"
}

过滤

屏幕截图 2024-05-04 212133.png

过滤是最常用的集合处理任务之一,在Kotlin中,过滤条件由谓词函数定义。

谓词函数:接受一个集合元素并且返回布尔值的 lambda 表达式, 其返回值含义:true 说明给定元素与谓词匹配,false 则表示不匹配

按谓词函数过滤

函数作用
filter返回与其匹配的集合元素,对于List和Set,过滤结果都是一个List,对Map来说结果还是一个Map
filterIndexed和filter作用一样,不同的是它带有元素在集合中的位置
filterNot与filter结果相反,它使用否定条件来过滤集合,会返回一个让谓词函数产生 false 的元素列表。
filterIsInstance返回指定 T 类型的元素集合
filterNotNull返回不为 null 的元素集合

代码示例如下:

val numbers = listOf("one", "two", "three", "four")
numbers.filter { it.length > 3 } // [three, four]

numbers.filterIndexed { index, s -> s.length > 3  } // [three, four]

numbers.filterNot { it.length <= 3 } // [three, four]

val numbers1 = listOf(null, 1, "two", 3.0, "four")
numbers1.filterIsInstance<String>() // 过滤掉不为 String 的元素,结果为[two, four]

numbers.filterNotNull() // 过滤掉null,结果为[1, two, 3.0, four]

划分

划分的函数只有一个,即 partition。 partition 会通过一个谓词函数过滤集合并且将不匹配的元素存放在一个单独的列表中,并保存在 Pair 中。代码如下所示

val numbers = listOf("one", "two", "three", "four")
// match 为 [three, four]
// rest 为 [one, two]
val (match, rest) = numbers.partition { it.length > 3 }

检验谓词函数

检验谓词函数有三个,分别为 any、none、all。作用是

函数作用
any如果至少有一个元素匹配给定谓词,那么 any() 返回 true
none如果没有元素与给定谓词匹配,那么 none() 返回 true
all如果所有元素都匹配给定谓词,那么 all() 返回 true。在一个空集合上使用任何有效的谓词函数去调用 all() 都会返回 true

代码示例如下:

val numbers = listOf("one", "two", "three", "four")
numbers.any { it.endsWith("e") }     //  true
numbers.none { it.endsWith("a") }   //  true
numbers.all { it.endsWith("e") }     //  false
emptyList<Int>().all { it > 5 }     //  true

加减

屏幕截图 2024-05-04 215915.png

在 Kotlin 中,为集合定义了 plus (+) 和 minus (-) 操作符。 它们把一个集合作为第一个操作数;第二个操作数可以是一个元素或者是另一个集合。 返回值是一个新的集合

代码示例如下:

val numbers = listOf("one", "three", "two", "three","four")
//需要注意,下面的操作都创建了新的集合
//相当于 add
numbers + "five" // [one, three, two, three, four, five]
//相当于 removeAll
numbers - listOf("three", "four") // [one, two]
//相对于 remove
numbers - "three" // [one, two, three, four]

val numbersMap = mapOf("one" to 1, "two" to 2, "three" to 3)
//相当于 put
numbersMap + Pair("four", 4) // {one=1, two=2, three=3, four=4}
//相当于 put
numbersMap.plus(Pair("one", 10)) // {one=10, two=2, three=3}
//相当于 putAll
numbersMap + mapOf("five" to 5, "one" to 11) // {one=11, two=2, three=3, five=5}
//相当于 remove
numbersMap - "one" // {two=2, three=3}
//相当于多次 remove
numbersMap - listOf("two", "four") // {one=1, three=3}
//右侧操作数不能是 map,如果传入map,则将map看成是单个key,会导致操作不成功
numbersMap - mapOf("one" to 1) // {one=1, two=2, three=3}

分组

屏幕截图 2024-05-04 215945.png

Kotlin 中分组的函数就两个,分别是 groupBy 和 groupingBy。groupBy 会根据传入的函数值作为 key 来分组,返回的是一个 Map。而 groupingBy 返回的是 Grouping,通过它可以对所有的组进行操作。

Grouping 支持的操作如下:

  • eachCount() 计算每个组中的元素。
  • fold() 与 reduce() 对每个组分别执行 fold 与 reduce 操作,作为一个单独的集合并返回结果。
  • aggregate() - 随后将给定操作应用于每个组中的所有元素并返回结果。 这是对 Grouping 执行任何操作的通用方法。当折叠或缩小不够时,可使用它来实现自定义操作。

代码示例如下:

val numbers = listOf("one", "two", "three", "four", "five")

// {O=[one], T=[two, three], F=[four, five]}
numbers.groupBy { it.first().toUpperCase() } 

// {o=[ONE], t=[TWO, THREE], f=[FOUR, FIVE]}
numbers.groupBy(
    keySelector = { it.first() }, //生成 key
    valueTransform = { it.toUpperCase() } //生成 value
)

// {o=1, t=2, f=2}
numbers.groupingBy { it.first() }.eachCount()

取集合的一部分

屏幕截图 2024-05-04 222549.png

slice

slice 返回具有给定索引的集合元素列表。 索引既可以是作为区间传入的也可以是作为整数值的集合传入的。返回列表的顺序由输入的索引顺序决定。

代码示例如下:

val numbers = listOf("one", "two", "three", "four", "five", "six")    
numbers.slice(1..3) // [two, three, four]
numbers.slice(0..4 step 2) // [one, three, five]
numbers.slice(setOf(3, 5, 0)) // [four, six, one]

take 与 drop

函数作用
take从头开始获取指定数量的元素,当调用的数字大于集合的大小时,将返回整个集合
takeLast从尾开始获取指定数量的元素,当调用的数字大于集合的大小时,将返回整个集合
takeWhile带有判断条件的 take,它会从头开始不断获取元素,直到不满足判断条件
takeLastWhile带有判断条件的 takeLast,它会从尾开始不断获取元素,直到不满足判断条件
drop从头去除给定数量的元素
dropLast从尾去除给定数量的元素
dropWhile与 takeWhile 相反,返回第一个不满足条件到末尾之间的元素
dropLastWhile与 takeLastWhile 相反,返回从开头到最后一个不满足条件之间的元素

代码示例如下:

val numbers = listOf("one", "two", "three", "four", "five", "six")

numbers.take(3) // [one, two, three]
numbers.takeLast(3) // [four, five, six]
numbers.takeWhile { !it.startsWith('f') } // [one, two, three]
numbers.takeWhile { it.startsWith('f') } // [] 返回空
numbers.takeLastWhile { it != "three" } // [four, five, six]

numbers.drop(1) // [two, three, four, five, six]
numbers.dropLast(5) // [one]
numbers.dropWhile { it.length == 3 })  // [three, four, five, six]
numbers.dropLastWhile { it.contains('i') } // [one, two, three, four]

chunked

chunked 可以将集合分解为给定大小的块。代码示例如下:

val numbers = listof(1, 2, 3, 4, 5, 6)
numbers.chunked(3) // [[0, 1], [2, 3], [4, 5]]

//对每个块进行求和操作,输出 [1, 5, 9]
numbers.chunked(3) { it.sum() } 

windowed 和 zipWithNext

windowed 可以获取所有的指定指定大小的窗口的集合,代码示例如下:

val numbers = listOf("one", "two", "three", "four", "five")
//内容为 [[one, two, three], [two, three, four], [three, four, five]]
numbers.windowed(3)

特殊的 zipWithNext 函数会获取两个大小窗口的集合,代码示例如下

val numbers = listOf("one", "two", "three", "four", "five")
// [(one, two), (two, three), (three, four), (four, five)]
numbers.zipWithNext()

// 也可以通过转换函数来调用;它应该以接收者集合的两个元素作为参数。
// 内容为 [false, false, true, false]
numbers.zipWithNext { s1, s2 -> s1.length > s2.length }

获取集合单个元素

屏幕截图 2024-05-05 110222.png

按位置取

按位置取是通过 index 获取指定元素,注意这些函数是针对 list、set的。函数介绍如下:

函数作用
elementAt获取指定位置的元素
elementAtOrNull当指定位置超出集合范围时,elementAtOrNull() 返回 null
elementAtOrElse当指定位置超出集合范围时,会返回传入函数的结果

代码示例如下:

val numbers = listOf("one", "two", "three", "four", "five")

numbers.elementAt(0) // one

numbers.elementAtOrNull(5) // null

// 返回为 The value for index 5 is undefined
numbers.elementAtOrElse(5) { index -> "The value for index $index is undefined"}

按条件取

函数作用
first获取满足条件的第一个元素,默认是直接获取第一个元素。没找到会抛出异常
firstOrNull和 first一样,区别是如果找不到匹配的元素,会返回 null
last获取满足条件的最后一个元素,默认是直接获取最后一个元素。没找到会抛出异常
lastOrNull和 last,区别是如果找不到匹配的元素,会返回 null
find等同于 firstOrNull
findLast等同于 lastOrNull
firstNotNullOf先对集合转化,然后再获取满足条件的第一个元素。没找到会抛出异常
firstNotNullOfOrNull和 firstNotNullOf 一样,区别是如果找不到匹配的元素,会返回 null

代码示例如下:

val numbers = listOf("one", "two", "three", "four", "five", "six")
numbers.first { it.length > 3 } // three
numbers.last { it.startsWith("f") } // five
numbers.firstOrNull { it.length > 6 } // null
numbers.find { it % 2 == 0 } // 2
numbers.firstNotNullOf { item -> item.takeIf { it.length >= 4 } } // three

随机取元素

random 可以检索集合的一个随机元素。代码示例如下:

val numbers = listOf(1, 2, 3, 4)

// 随机到 2
numbers.random()

// 在 0 到 1的范围内取值,不包括1
// 只能获取到 1
numbers.random(Random(1))   

检测元素存在与否

函数作用
contains检查集合中某个元素是否存在,可以使用 in 关键字以操作符的形式调用 contains
containsAll检查是否包含其他集合的元素
isEmpty检查集合是否为空
isNotEmpty检查集合是否不为空

代码示例如下:

val numbers = listOf("one", "two", "three", "four", "five", "six")

//等同于 "four" in numbers
numbers.contains("four") // true
numbers.containsAll(listOf("four", "two")) // true
numbers.isEmpty() // false
numbers.isNotEmpty() // true

集合排序

屏幕截图 2024-05-04 224940.png

sorted

sorted 相关的排序函数介绍如下:

函数作用
sorted使集合元素按照其自然顺序升序排序
sortedDescending使集合元素按照其自然顺序降序排序
sortedBy通过函数返回值的自然顺序升序排序
sortedByDescending通过函数返回值的自然顺序降序排序
sortedWith通过传入的 Comparator 比较器来排序

代码示例如下:

val numbers = listOf("one", "two", "three", "four")

numbers.sorted() // 升序  [four, one, three, two]

numbers.sortedDescending() // 降序  [two, three, one, four]

numbers.sortedBy { it.length } // 根据长度升序 [one, two, four, three]

numbers.sortedByDescending { it.last() } // 根据最后一个字符倒叙 [four, two, one, three]

val lengthComparator = Comparator { str1: String, str2: String -> str1.length - str2.length }
numbers.sortedWith(lengthComparator)// 根据比较器排序 [one, two, four, three]

reversed

reversed 和 asReversed 函数都会返回倒序后的集合。它们的区别是,对于可变集合,对 asReversed 返回集合的修改会影响原集合,而 reversed 不会。而代码示例如下

val numbers = listOf("one", "two", "three", "four")
numbers.reversed()
numbers.asReversed()

shuffled

shuffled 函数会返回以随机顺序排序的集合元素的新的 List。代码示例如下:

val numbers = listOf("one", "two", "three", "four")
numbers.shuffled() // [four, three, one, two]

集合聚合操作

屏幕截图 2024-05-05 162628.png

min 、 max、average 、sum 和 count

函数作用
min返回集合中最小值
minBy接受一个选择器函数并返回使选择器返回的最小值的元素
minWith接受一个 Comparator 对象并且根据此 Comparator 对象返回最小元素
minOf获取函数处理集合元素后最小的值
max返回集合中最大值
maxBy接受一个选择器函数并返回使选择器返回的最大的元素
maxWith接受一个 Comparator 对象并且根据此 Comparator 对象返回最大元素
maxOf获取函数处理集合元素后最大的值
average返回数字集合中元素的平均值
sum返回数字集合中元素的总和
sumOf累加函数处理集合元素后的值
count返回集合中元素的数量

代码示例如下

val numbers = listOf(6, 42, 10, 4)
println("Min: ${numbers.min()}") // Min: 4
println("Max: ${numbers.max()}") //Max: 42

val numbers = listOf(5, 42, 10, 4)
val min3Remainder = numbers.minBy { it % 3 }
println(min3Remainder) //输出结果:42 

val strings = listOf("one", "two", "three", "four")
val longestString = strings.maxWith(compareBy { it.length })
println(longestString) //输出结果:three

val list = listOf(1, 2, 3)  
println(list.minOf { it * 3 })//输出结果为 3

println("Average: ${numbers.average()}") // 输出结果为 Average: 15.5
println("Sum: ${numbers.sum()}") // 输出结果为 Sum: 62

val numbers = listOf("one", "two", "three", "four")  
println(numbers.sumOf { it.length }) //输出结果为 15

需要注意,min、minBy、minWith、minOf 函数在空集合中使用时会报错,这时我们可以使用 minOrNull、minByOrNull、minWithOrNull、minOfOrNull,在空集合中,它们会返回 null。max 的函数也是一样的。

reduce 和 fold

reduce 和 fold 函数作用都是依次将所提供的操作函数应用于集合元素并返回累积的结果。不同的是 fold 函数会接受一个初始值并将其用作第一步的累积值;而 reduce 函数没有。

代码示例如下:

val numbers = listOf(5, 2, 10, 4)  
val simpleSum = numbers.reduce { sum, element -> sum + element }  
println(simpleSum) //结果为 5 + 2 + 10 + 4 = 21
val sumDoubled = numbers.fold(10) { sum, element -> sum + element * 2 }  
println(sumDoubled) //结果为 10 + 5 * 2 + 2 * 2 + 10 * 2 + 4 * 2 = 52

如果想要以相反的顺序应用于元素,可以使用 reduceRight 和 foldRight 函数,代码示例如下:

val numbers = listOf(5, 2, 10, 4)  
// 注意注意 element, sum 的参数位置,foldRight 与 fold 是相反的
val sumDoubledRight = numbers.foldRight(1) { element, sum -> sum * element }  
println(sumDoubledRight)// 输出为 1 * 4 * 10 * 2 * 5 = 400

特别注意:sum, element 的参数位置,reduceRight 、foldRight 与 fold 、reduce 是相反的。 在fold 、reduce 中是 sum, element,而在 reduceRight 、foldRight 中是 element, sum

如果你想要获取到当前的index,就需要使用 reduceIndexed 和 foldIndexed 函数。它们也有对应的相反顺序的函数 reduceRightIndexed 与 foldRightIndexed。代码示例如下:

val sumEven = numbers.reduceRightIndexed { idx, element, sum ->  
    println("$idx $sum $element")  
    if (idx % 2 == 0) sum + element else sum  
}  
println(sumEven)  //结果为 7
println("====================================")  
val sumEvenRight = numbers.foldRightIndexed(0) { idx, element, sum ->  
    println("$idx $sum $element")  
    if (idx % 2 == 0) sum + element else sum  
}  
println(sumEvenRight) //结果为 15

所有的 reduce 函数会在空集合上使用都会报错。如果想要在空集合时返回null,则可以使用 reduceOrNull、reduceRightOrNull、reduceIndexedOrNull、reduceRightIndexedOrNull 。

如果你想要获取计算过程中的中间结果就可以使用 runningReduce、runningReduceIndexed、runningFold、runningFoldIndexed。代码示例如下:

val numbers = listOf(0, 1, 2, 3, 4, 5)  
//结果为 [0, 1, 3, 6, 10, 15]
val runningReduceSum = numbers.runningReduce { sum, item -> sum + item }  
//结果为 [10, 10, 11, 13, 16, 20, 25]
val runningFoldSum = numbers.runningFold(10) { sum, item -> sum + item }

参考