15、Collections KTX:Kotlin集合框架的KTX扩展,如associateBy、groupBy
说实话,Kotlin的集合操作是我每天写代码时最离不开的部分。尤其是 associateBy 和 groupBy 这两个扩展函数,它们就像瑞士军刀里的两把主力刀片——一个帮你快速建立索引,一个帮你做数据分组。今天我就带你彻底搞懂它们。
为什么需要这两个扩展?
先问个问题:你手头有一个用户列表,想通过用户ID快速找到某个用户,你会怎么做?
最原始的做法是遍历列表,一个个比对。但这样每次查找都是O(n)复杂度,数据量一上来就卡了。更好的做法是把列表转成Map,用ID做key,用户对象做value。这样查找就变成了O(1)。
嗯,associateBy 就是干这个的。而 groupBy 呢?它解决的是「一对多」的问题——比如按年龄分组,同一个年龄的人放到一个列表里。
核心区别一句话:
associateBy:每个key对应一个元素(key必须唯一)groupBy:每个key对应一个元素列表(key可以重复)
associateBy:建立一对一索引
我在项目中经常用 associateBy 来缓存数据。比如从服务器拉取了一堆商品信息,我需要根据商品ID快速更新某个商品的状态。
data class Product(
val id: Long,
val name: String,
val price: Double
)
val products = listOf(
Product(1, "手机", 2999.0),
Product(2, "耳机", 199.0),
Product(3, "充电器", 49.0)
)
// 用ID做key,建立索引
val productMap: Map<Long, Product> = products.associateBy { it.id }
// 现在查找就很快了
val target = productMap[2] // 直接拿到耳机
println(target?.name) // 输出:耳机
这里有个坑,我曾经踩过:如果列表里有重复的key,associateBy 会保留最后一个出现的元素。前面的会被覆盖掉。
我曾经遇到过的问题:
有一次从数据库查数据,ID字段理论上唯一,但有个bug导致出现了重复ID。用 associateBy 后,数据莫名其妙少了。排查了半天才发现是数据源的问题。所以建议:如果你不确定key是否唯一,先用 distinctBy 去重,或者改用 groupBy 再取第一个。
associateBy 的变体
除了基本用法,还有几个变体也很实用:
// 只保留value的某个字段
val nameMap: Map<Long, String> = products.associateBy(
keySelector = { it.id },
valueTransform = { it.name }
)
// 结果:{1=手机, 2=耳机, 3=充电器}
// 如果key可能为null,用associateByNotNull
val nullableMap = products.associateBy { it.name.takeIf { it.length > 1 } }
// 这个会过滤掉key为null的条目
我个人习惯用 associateBy 配合 valueTransform 来提取数据。比如从用户列表里提取「用户ID -> 用户昵称」的映射,这样既保留了索引,又减少了内存占用。
groupBy:一对多分组
说白了,groupBy 就是按某个条件把数据分到不同的桶里。每个桶是一个列表,桶的标签就是key。
data class Student(
val name: String,
val grade: Int, // 年级
val score: Int
)
val students = listOf(
Student("张三", 1, 85),
Student("李四", 1, 92),
Student("王五", 2, 78),
Student("赵六", 2, 88),
Student("钱七", 3, 95)
)
// 按年级分组
val byGrade: Map<Int, List<Student>> = students.groupBy { it.grade }
// 遍历每个年级的学生
byGrade.forEach { (grade, list) ->
println("$grade 年级有 ${list.size} 个学生")
}
你想想看,这个功能在报表统计里有多好用。我做过一个成绩分析系统,用 groupBy 按班级分组,然后对每个组计算平均分、最高分、最低分,代码写起来非常清爽。
groupBy 的进阶用法
// 分组后只保留需要的字段
val nameByGrade: Map<Int, List<String>> = students.groupBy(
keySelector = { it.grade },
valueTransform = { it.name }
)
// 结果:{1=[张三, 李四], 2=[王五, 赵六], 3=[钱七]}
// 分组后对每组做聚合
val scoreStats: Map<Int, Int> = students.groupBy(
keySelector = { it.grade },
valueTransform = { it.score }
).mapValues { (_, scores) ->
scores.average().toInt()
}
// 结果:{1=88, 2=83, 3=95}
小技巧:
如果你需要分组后对每组做复杂计算,可以结合 groupingBy 和 fold 或 aggregate。比如计算每个年级的总分和人数:
val result = students.groupingBy { it.grade }
.fold(0 to 0) { acc, student ->
(acc.first + student.score) to (acc.second + 1)
}
// 结果:{1=(177, 2), 2=(166, 2), 3=(95, 1)}
associateBy vs groupBy:如何选择?
| 场景 | 用哪个 | 原因 |
|---|---|---|
| 通过ID查找单个对象 | associateBy |
key唯一,直接映射 |
| 按类别统计数量 | groupBy |
一个类别可能有多个元素 |
| 提取字段映射 | associateBy + valueTransform |
一对一映射,简洁高效 |
| 数据报表/聚合 | groupBy + mapValues |
分组后做统计计算 |
| key可能重复但想保留所有 | groupBy |
不会丢失数据 |
| key可能重复但只取一个 | associateBy(保留最后一个) |
或者用 distinctBy 预处理 |
实战:一个完整的例子
我记得有一次做订单管理系统,需要把订单按状态分组,同时还要能通过订单号快速查找。这两个扩展正好派上用场:
data class Order(
val orderId: String,
val userId: Long,
val status: String, // "pending", "paid", "shipped", "completed"
val amount: Double
)
val orders = listOf(
Order("A001", 1, "paid", 299.0),
Order("A002", 2, "pending", 59.0),
Order("A003", 1, "paid", 199.0),
Order("A004", 3, "shipped", 899.0),
Order("A005", 2, "completed", 129.0)
)
// 1. 按状态分组,方便统计
val ordersByStatus: Map<String, List<Order>> = orders.groupBy { it.status }
println("待支付订单数:${ordersByStatus["pending"]?.size ?: 0}")
// 2. 建立订单号索引,方便快速查找
val orderIndex: Map<String, Order> = orders.associateBy { it.orderId }
val targetOrder = orderIndex["A003"]
println("订单A003的金额:${targetOrder?.amount}")
// 3. 按用户分组,查看每个用户的订单
val ordersByUser: Map<Long, List<Order>> = orders.groupBy { it.userId }
ordersByUser.forEach { (userId, userOrders) ->
val total = userOrders.sumOf { it.amount }
println("用户$userId 总消费:$total")
}
你看,三个需求,三行核心代码就搞定了。要是用Java写,至少得写十几个循环和临时变量。
性能与注意事项
虽然这两个扩展用起来很爽,但有些细节还是要注意:
- 内存占用:
associateBy和groupBy都会创建新的Map对象。如果列表很大(比如几十万条),会占用不少内存。我建议只在需要频繁查找的场景下使用,用完就释放引用。 - 懒加载:这两个函数都是立即执行的(eager),不是懒加载。如果你只需要部分数据,考虑用
sequence配合associateBy或groupBy。 - null key:
associateBy和groupBy都允许key为null。但Map里只能有一个null key。如果你不确定key是否可能为null,可以用associateByNotNull或groupBy后手动处理。
我的建议:
在Android开发中,如果你需要频繁从列表里查找数据,比如在Adapter的 bind 方法里,强烈建议在列表数据更新时,提前用 associateBy 建立索引。这样每次bind时直接查Map,比遍历列表快得多。我优化过一个列表卡顿问题,就是靠这个思路把滑动帧率从40fps提到了60fps。
知识体系总览
下面这张图帮你理清这两个扩展的关系和适用场景:
说白了,这两个扩展就是让你用声明式的方式处理集合,而不是写一堆for循环。我个人觉得,掌握它们之后,代码的可读性和维护性都上了一个台阶。你想想看,一行代码能搞定的事情,为什么要写十行呢?
最后一个小建议:
刚开始用的时候,可能会搞混 associateBy 和 groupBy 的返回值类型。记住一个口诀:「associateBy 返回 Map<K, T>,groupBy 返回 Map<K, List<T>>」。多写几次就自然记住了。