Kotlin 集合
集合是日常编程的核心——一组数据要存储、过滤、转换、聚合。Kotlin 的集合 API 设计极其优雅,明确区分只读与可变,提供丰富的函数式操作(map、filter、groupBy、chunked、windowed 等),写起来像声明式 SQL,远比 Java Stream 直观。
1. List(列表,有序可重复)
// 只读 List:listOf
val nums: List<Int> = listOf(1, 2, 3, 4, 5)
println(nums.size) // 5
println(nums[0]) // 1(支持索引访问)
println(nums.first()) // 1
println(nums.last()) // 5
println(nums.indexOf(3)) // 2
// 可变 List:mutableListOf
val mut: MutableList<Int> = mutableListOf(1, 2, 3)
mut.add(4) // [1, 2, 3, 4]
mut.add(0, 0) // [0, 1, 2, 3, 4]
mut.removeAt(0) // [1, 2, 3, 4]
mut.remove(2) // [1, 3, 4]
mut[0] = 100 // [100, 3, 4]
println(mut)
// 只读 vs 可变:
// List 接口【不暴露 add/remove】
// 但底层可能仍是可变实现
// 强烈推荐:能用 List 就别用 MutableList(更安全)关键:Kotlin 把 List 接口设计成只读(没有 add/remove 方法),可变版本叫 MutableList。这是和 Java 最大的区别——Java 的 List 接口暴露 add/remove,导致任何人拿到 List 都可能改它,Kotlin 在类型层面就拦住了误改。
实际开发建议:能用 List 就别用 MutableList。只在确实需要修改时用 MutableList,且尽量缩小可变范围。
2. Set(集合,无序不重复)
// Set:不重复元素的集合
val set1: Set<String> = setOf("苹果", "香蕉", "苹果")
println(set1) // [苹果, 香蕉](自动去重)
println(set1.size) // 2
println(set1.contains("苹果")) // true
println("苹果" in set1) // true(in 运算符)
// 可变 Set
val mutSet: MutableSet<Int> = mutableSetOf(1, 2, 3)
mutSet.add(4)
mutSet.add(1) // 无效,1 已存在
mutSet.remove(2)
println(mutSet) // [1, 3, 4]
// 集合运算
val a = setOf(1, 2, 3, 4)
val b = setOf(3, 4, 5, 6)
println(a union b) // 并集 [1, 2, 3, 4, 5, 6]
println(a intersect b) // 交集 [3, 4]
println(a subtract b) // 差集 [1, 2]Set 用于去重、快速查找、集合运算(并集/交集/差集)。Kotlin 用 union、intersect、subtract 这几个 infix 函数,可读性极强。
3. Map(映射,键值对)
// 只读 Map:mapOf
val map: Map<String, Int> = mapOf(
"苹果" to 5,
"香蕉" to 3,
"橙子" to 7
)
// 访问:用 []
println(map["苹果"]) // 5
println(map.get("梨")) // null(键不存在)
// 用 in 判断键存在
println("苹果" in map) // true
// 遍历
for ((key, value) in map) {
println("$key -> $value")
}
// 常用属性
println(map.size) // 3
println(map.keys) // [苹果, 香蕉, 橙子]
println(map.values) // [5, 3, 7]
// 可变 Map
val mutMap = mutableMapOf("a" to 1)
mutMap["b"] = 2 // 添加/更新
mutMap.put("c", 3) // 同上,Java 风格
mutMap.remove("a")
println(mutMap) // {b=2, c=3}Map 的创建用 to infix 函数:"键" to 值。这比 Java 的 map.put("键", 值) 优雅得多。map[key] 访问,键不存在返回 null(不会抛异常)。
4. 只读集合的查询函数
Kotlin 把所有常用查询做成了统一接口(List/Set/Map 都有),且支持 lambda:
// 只读集合的查询函数
val list = listOf(1, 2, 3, 4, 5)
// 计数
println(list.size) // 5
println(list.count { it > 2 }) // 3(满足条件的数量)
// 查找
println(list.find { it > 3 }) // 4(第一个满足的,可能 null)
println(list.findLast { it < 5 }) // 4(最后一个)
println(list.first { it > 2 }) // 3(找不到抛异常)
println(list.firstOrNull { it > 100 }) // null(安全)
// 判断
println(list.all { it > 0 }) // true(全部满足)
println(list.any { it > 4 }) // true(至少一个满足)
println(list.none { it < 0 }) // true(全不满足)
// 包含
println(3 in list) // true
println(list.contains(10)) // false注意 first vs firstOrNull:前者找不到抛异常,后者返回 null。生产代码尽量用 OrNull 后缀的版本。
5. 转换操作(map、flatMap、fold)
// 转换(transform)操作
val list = listOf(1, 2, 3, 4, 5)
// map:每个元素转换为新元素
val doubled = list.map { it * 2 }
println(doubled) // [2, 4, 6, 8, 10]
// mapIndexed:带索引
val withIndex = list.mapIndexed { i, v -> "[$i]=$v" }
println(withIndex) // [[0]=1, [1]=2, ...]
// flatMap:每个元素展开为多个元素
val nested = listOf(listOf(1, 2), listOf(3, 4))
val flat = nested.flatten() // [1, 2, 3, 4]
val flat2 = nested.flatMap { it } // 同上
// 累加
val sum = list.sum() // 15
val product = list.reduce { acc, n -> acc * n } // 120(无初始值)
val product2 = list.fold(1) { acc, n -> acc * n } // 120(有初始值)这些是函数式编程的核心。Java 8 引入 Stream API 才有这些(且要先 .stream() 再 .collect()),Kotlin 直接是集合方法,用起来更顺手。
- map:一对一转换。
- flatMap:一对多展开,适合处理嵌套结构。
- reduce / fold:累加。fold 有初始值,reduce 没有。
6. 过滤操作(filter、take、drop)
// 过滤(filter)操作
val list = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
// filter:保留满足条件的
val evens = list.filter { it % 2 == 0 }
println(evens) // [2, 4, 6, 8, 10]
// filterNot:反向
val odds = list.filterNot { it % 2 == 0 }
println(odds) // [1, 3, 5, 7, 9]
// take / drop
println(list.take(3)) // [1, 2, 3](前 3 个)
println(list.takeLast(2)) // [9, 10](后 2 个)
println(list.drop(3)) // [4, 5, 6, 7, 8, 9, 10](跳过前 3)
println(list.dropLast(2)) // [1, ..., 8](跳过后 2)
// 条件取
println(list.takeWhile { it < 4 }) // [1, 2, 3](遇到不满足停止)
println(list.dropWhile { it < 4 }) // [4, 5, ...]
// distinct:去重
val dup = listOf(1, 1, 2, 3, 3, 3)
println(dup.distinct()) // [1, 2, 3]这些函数返回新集合(不改原集合),非常适合链式调用:list.filter { it > 0 }.map { it * 2 }.sorted()。这种声明式写法读起来像"做什么"而非"怎么做",可读性远强于 for + if。
7. 排序与分组
// 排序
val list = listOf(3, 1, 4, 1, 5, 9, 2, 6)
println(list.sorted()) // [1, 1, 2, 3, 4, 5, 6, 9]
println(list.sortedDescending()) // [9, 6, 5, 4, 3, 2, 1, 1]
println(list.sortedBy { -it }) // [9, 6, 5, 4, 3, 2, 1, 1](按变换排序)
println(list.sortedDescendingBy { it }) // 同 sortedDescending
// 对象按字段排序
data class Score(val name: String, val score: Int)
val scores = listOf(Score("A", 90), Score("B", 85), Score("C", 95))
val byScore = scores.sortedByDescending { it.score }
println(byScore) // [Score(C, 95), Score(A, 90), Score(B, 85)]
// 分组
val grouped = list.groupBy { it % 3 }
println(grouped) // {1=[1, 1, 4], 2=[2, 5], 0=[3, 9, 6]}
val byPass = scores.groupBy { it.score >= 90 }
println(byPass)
// {true=[Score(A, 90), Score(C, 95)], false=[Score(B, 85)]}Kotlin 排序 API 极丰富:sorted、sortedBy、sortedDescending、sortedByDescending、sortedWith(自定义比较器)。分组用 groupBy 返回 Map<Key, List<Value>>,一行搞定分类。
8. 高级操作:分块、窗口、拉链
Kotlin 还有几个 Java Stream 没有的甜点函数:
// 分块、窗口
val list = listOf(1, 2, 3, 4, 5, 6, 7)
// chunked:固定大小分块
val chunks = list.chunked(3)
println(chunks) // [[1, 2, 3], [4, 5, 6], [7]]
// windowed:滑动窗口
val windows = list.windowed(3)
println(windows) // [[1, 2, 3], [2, 3, 4], [3, 4, 5], ...]
// windowed 带步长
val step2 = list.windowed(3, step = 2)
println(step2) // [[1, 2, 3], [3, 4, 5], [5, 6, 7]]
// zip:拉链配对
val names = listOf("小明", "小红")
val ages = listOf(20, 22)
val pairs = names zip ages
println(pairs) // [(小明, 20), (小红, 22)]
val map = pairs.toMap()
println(map) // {小明=20, 小红=22}
// partition:按条件分成两个
val (even, odd) = listOf(1, 2, 3, 4, 5).partition { it % 2 == 0 }
println(even) // [2, 4]
println(odd) // [1, 3, 5]- chunked(n):按 n 个一组分块,适合批量处理(如分页)。
- windowed(n):滑动窗口,适合时间序列分析、移动平均。
- zip(other):两个集合配对成 Pair,适合合并数据。
- partition:按条件分成两个集合(满足 / 不满足),返回 Pair。
9. 序列(Sequence,惰性求值)
Kotlin 还有 Sequence,类似 Java Stream,是惰性的——多个操作合在一次遍历完成,适合大集合或多步链式操作:
val list = (1..1_000_000).toList()
// Eager:每步都创建中间集合
val r1 = list.filter { it % 2 == 0 }.map { it * 2 }.take(10)
// 创建 ~500000 元素的中间集合,然后只取 10 个,浪费
// Lazy:用 asSequence() 转序列,所有步骤合并
val r2 = list.asSequence()
.filter { it % 2 == 0 }
.map { it * 2 }
.take(10)
.toList()
// 只遍历到找到 10 个就停,效率高
// 经验:小集合用 List API(更直观),大集合或多步链用 Sequence
println(r1)
println(r2)10. 实战:学生成绩统计
data class Student(val name: String, val score: Int, val grade: String)
val students = listOf(
Student("小明", 92, "A"),
Student("小红", 88, "B"),
Student("小刚", 75, "C"),
Student("小李", 95, "A"),
Student("小张", 60, "D")
)
// 平均分
val avg = students.map { it.score }.average()
println("平均: ${"%.1f".format(avg)}") // 平均: 82.0
// 按等级分组
val byGrade = students.groupBy { it.grade }
byGrade.forEach { (grade, list) ->
println("$grade 等级: ${list.map { it.name }}")
}
// A 等级: [小明, 小李]
// B 等级: [小红]
// ...
// Top 3
val top3 = students.sortedByDescending { it.score }.take(3)
println("Top 3: ${top3.map { it.name }}") // [小李, 小明, 小红]
// 姓名列表
val names = students.map { it.name }
println("所有学生: $names")一个真实的数据处理任务,Kotlin 几行就能搞定,可读性极强。对比 Java 的 for + if 风格,这种声明式代码更易维护、更易测试。
11. 集合 vs 数组
Kotlin 的 Array 和集合是两套不同的 API:
- 数组(Array<Int>):固定大小,基本类型版本(IntArray)性能最优,但 API 较少。
- 集合(List<Int>):动态大小,API 极丰富(map、filter、sorted...),日常首选。
原则:99% 场景用集合,只在性能敏感(如大量数值计算)或与 Java 互操作时用数组。
小结
这一章你掌握了 Kotlin 集合的所有姿势:List、Set、Map 的只读/可变版本;map、filter、sorted、groupBy、chunked 等函数式操作;Sequence 惰性求值。下一篇进入 Kotlin 的面向对象——类与对象。
← 上一篇 Kotlin 字符串
下一篇 Kotlin 类与对象 →