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 用 unionintersectsubtract 这几个 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 直接是集合方法,用起来更顺手。

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 极丰富:sortedsortedBysortedDescendingsortedByDescendingsortedWith(自定义比较器)。分组用 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]

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:

原则:99% 场景用集合,只在性能敏感(如大量数值计算)或与 Java 互操作时用数组。

小结

这一章你掌握了 Kotlin 集合的所有姿势:List、Set、Map 的只读/可变版本;map、filter、sorted、groupBy、chunked 等函数式操作;Sequence 惰性求值。下一篇进入 Kotlin 的面向对象——类与对象。

← 上一篇 Kotlin 字符串

下一篇 Kotlin 类与对象

✈️💬