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 作为底层实现,既能去重又能保持元素插入顺序。

🔗 参考文档:Iterabledistinct()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,也可以是 IntPair 甚至复合对象。

例如,按“端口奇偶性 + 协议”组合去重:

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


原始标题:Removing Duplicate Elements From Collections in Kotlin