1. 概述
在 Kotlin 开发中,处理集合时经常会遇到需要去除重复元素的场景。本文将系统讲解如何使用标准库提供的方法高效实现去重,并深入其底层机制,帮助你在实际项目中避免踩坑。
无论是简单的字符串列表,还是复杂的自定义对象,Kotlin 都提供了简洁且强大的 API 支持。
2. 使用 distinct()
去重
✅ distinct()
是最常用的去重函数,适用于基于对象相等性(equality)判断重复的标准场景。
它可用于任意实现了 Iterable
接口的集合类型,返回一个不含重复元素的新 List
。
val protocols = listOf("tcp", "http", "tcp", "udp", "udp")
val distinct = protocols.distinct()
assertThat(distinct).hasSize(3)
assertThat(distinct).containsExactlyInAnyOrder("tcp", "http", "udp")
如上所示,distinct()
成功保留了每个唯一值的一份副本,原始顺序中的首次出现会被保留。
2.1 实现原理分析
distinct()
的核心逻辑非常直观:
public fun <T> Iterable<T>.distinct(): List<T> {
return this.toMutableSet().toList()
}
⚠️ 它实际上是通过将原集合转为 Set
来实现去重 —— 利用 Set
不允许重复元素的特性。
这意味着:
- 元素之间的“是否重复”由其
equals()
方法决定 - 因此,必须保证
equals()
和hashCode()
实现符合规范且相互兼容
📌 当前 Kotlin 使用的是 LinkedHashSet
作为底层实现,既能去重又能保持元素插入顺序。
🔗 参考文档:Iterable|distinct()|List
❌ 踩坑提醒:如果你对自定义类未正确重写 equals()
和 hashCode()
,即使内容相同也可能无法去重!
例如以下代码就会出问题:
class Url(val host: String) // ❌ 没有 data class,也没有重写 equals/hashCode
val urls = listOf(Url("baeldung"), Url("baeldung"))
println(urls.distinct().size) // 输出 2,而不是期望的 1
✅ 正确做法是使用 data class
或手动实现 equals/hashCode
。
3. 使用 distinctBy()
按条件去重
有时候我们并不想依赖整个对象的相等性,而是希望根据某个字段或计算结果来判断是否重复。
比如,你只想保留不同主机名的 URL,而忽略协议或路径差异 —— 这就是 distinctBy()
的用武之地。
3.1 基本用法
先定义一个 Url
类:
data class Url(val protocol: String, val host: String, val port: Int, val path: String)
准备测试数据:
val urls = listOf(
Url("https", "baeldung", 443, "/authors"),
Url("https", "baeldung", 443, "/authors"),
Url("http", "baeldung", 80, "/authors"),
Url("https", "baeldung", 443, "/kotlin/distinct"),
Url("https", "google", 443, "/"),
Url("http", "google", 80, "/search"),
Url("tcp", "docker", 2376, "/"),
)
现在,如果我们只关心 host 是否重复,可以这样写:
val uniqueHosts = urls.distinctBy { it.host }
assertThat(uniqueHosts).hasSize(3) // baeldung, google, docker
这里 distinctBy { it.host }
表示:比较时只看 host
字段的值。
再举个复杂点的例子:按“完整服务地址”去重(协议 + 主机 + 端口):
val uniqueUrls = urls.distinctBy { "${it.protocol}://${it.host}:${it.port}/" }
assertThat(uniqueUrls).hasSize(5)
这样,即使是不同路径的请求,只要它们指向同一个服务端点(endpoint),就视为重复项被去除。
3.2 底层实现解析
来看 distinctBy()
的源码实现:
public inline fun <T, K> Iterable<T>.distinctBy(selector: (T) -> K): List<T> {
val set = HashSet<K>()
val list = ArrayList<T>()
for (e in this) {
val key = selector(e)
if (set.add(key))
list.add(e)
}
return list
}
关键点如下:
- ✅ 只遍历一次,时间复杂度 O(n),效率高
- 使用一个临时
HashSet<K>
存储提取出的“比较键” selector(e)
返回的 key 若已存在,则跳过该元素- 最终返回的是原始元素组成的
List<T>
,而非 key 列表
📌 注意:selector
lambda 中可以返回任意类型 K
,不限于 String
,也可以是 Int
、Pair
甚至复合对象。
例如,按“端口奇偶性 + 协议”组合去重:
urls.distinctBy { Pair(it.protocol, it.port % 2) }
4. 总结
方法 | 适用场景 | 关键依赖 |
---|---|---|
distinct() |
使用对象整体相等性判断重复 | equals() / hashCode() |
distinctBy() |
自定义去重维度(如某字段、表达式) | selector 返回值的相等性 |
✅ 推荐实践:
- 对简单类型或
data class
直接用distinct()
- 需要按字段、表达式或业务逻辑去重时,优先选择
distinctBy()
- 自定义类务必确保
equals/hashCode
正确实现,否则去重失效
🔗 所有示例代码已上传至 GitHub:Baeldung/kotlin-tutorials - core-kotlin-collections-2