Appearance
Kotlin Set 集合:去重神器的奥秘 🎯
引言:为什么我们需要 Set?
想象一下,你正在开发一个问题追踪系统。用户不断提交各种问题描述,但你发现一个头疼的现象:同样的问题被重复提交了无数次! 📝
如果使用普通的 List 来存储这些问题,你会遇到什么困扰?
kotlin
// 使用 List 的痛苦经历
val issuesList = mutableListOf<String>()
issuesList.add("登录失败")
issuesList.add("页面加载慢")
issuesList.add("登录失败") // 重复了!
issuesList.add("登录失败") // 又重复了!
println("问题总数: ${issuesList.size}") // 输出: 4
// 但实际上只有 2 个不同的问题!这就是 Set 集合闪亮登场的时刻!它就像一个严格的门卫,绝不允许重复元素进入。
NOTE
Set 的核心哲学:唯一性至上!它专门解决"去重"这个在实际开发中极其常见的痛点。
Set 的本质与设计哲学
什么是 Set?
Set 是一个无序的、不允许重复元素的集合。它的设计灵感来源于数学中的"集合"概念,天生具备以下特性:
- ✅ 唯一性保证:同一个元素最多只能存在一次
- ✅ 高效查重:内部优化了重复检测机制
- ✅ 业务语义清晰:当你使用 Set 时,就明确表达了"这里不应该有重复"的意图
Set 解决的核心问题
在 SpringBoot 中的实战应用
场景一:问题追踪系统
让我们基于提供的示例,构建一个完整的 SpringBoot 问题追踪服务:
kotlin
@RestController
@RequestMapping("/api/issues")
class IssueTrackingController {
// 使用 MutableSet 存储唯一的问题描述
private val openIssues: MutableSet<String> = mutableSetOf(
"uniqueDescr1",
"uniqueDescr2",
"uniqueDescr3"
)
@PostMapping("/add")
fun addIssue(@RequestBody request: AddIssueRequest): ResponseEntity<IssueResponse> {
val isAdded = openIssues.add(request.description)
return ResponseEntity.ok(
IssueResponse(
description = request.description,
status = getStatusLog(isAdded),
totalUniqueIssues = openIssues.size
)
)
}
@GetMapping("/all")
fun getAllIssues(): ResponseEntity<Set<String>> {
return ResponseEntity.ok(openIssues.toSet()) // 返回只读视图
}
private fun getStatusLog(isAdded: Boolean): String {
return if (isAdded) {
"registered correctly."
} else {
"marked as duplicate and rejected."
}
}
}
data class AddIssueRequest(val description: String)
data class IssueResponse(
val description: String,
val status: String,
val totalUniqueIssues: Int
)TIP
注意 openIssues.add() 方法的返回值!它返回 Boolean:
true:元素成功添加(之前不存在)false:元素已存在,拒绝添加
场景二:用户标签管理系统
在实际业务中,Set 还常用于管理用户标签、权限等场景:
kotlin
@Entity
class User {
@ElementCollection
var tags: MutableList<String> = mutableListOf()
fun addTag(tag: String) {
// 需要手动检查重复 - 容易遗漏!
if (!tags.contains(tag)) {
tags.add(tag)
}
}
}kotlin
@Entity
class User {
@ElementCollection
var tags: MutableSet<String> = mutableSetOf()
fun addTag(tag: String): Boolean {
return tags.add(tag) // 自动去重,返回是否成功添加
}
fun addTags(newTags: Collection<String>) {
tags.addAll(newTags) // 批量添加,自动去重
}
}场景三:缓存键管理
kotlin
@Service
class CacheKeyManager {
private val activeKeys: MutableSet<String> = mutableSetOf()
@Cacheable("user-data")
fun getUserData(userId: String): UserData {
val cacheKey = "user:$userId"
activeKeys.add(cacheKey)
// 业务逻辑...
return fetchUserFromDatabase(userId)
}
@Scheduled(fixedRate = 300000) // 每5分钟清理一次
fun cleanupExpiredKeys() {
val expiredKeys = activeKeys.filter { isExpired(it) }
activeKeys.removeAll(expiredKeys.toSet())
log.info("清理了 ${expiredKeys.size} 个过期缓存键")
}
fun getActiveKeyCount(): Int = activeKeys.size
}Set 的核心操作与最佳实践
创建 Set 的多种方式
kotlin
// 1. 不可变 Set
val readOnlySet = setOf("A", "B", "C")
// 2. 可变 Set
val mutableSet = mutableSetOf<String>()
// 3. 从其他集合创建
val listWithDuplicates = listOf("A", "B", "A", "C", "B")
val uniqueSet = listWithDuplicates.toSet()
println(uniqueSet) // [A, B, C] - 自动去重!
// 4. 空 Set
val emptySet = emptySet<String>()
val emptyMutableSet = mutableSetOf<String>()常用操作详解
kotlin
@Service
class SetOperationsDemo {
fun demonstrateSetOperations() {
val fruits = mutableSetOf("apple", "banana", "orange")
// 添加元素
val added1 = fruits.add("grape") // true - 新元素
val added2 = fruits.add("apple") // false - 已存在
// 批量添加
fruits.addAll(listOf("kiwi", "apple", "mango")) // apple被忽略
// 检查存在性
val hasApple = fruits.contains("apple") // true
val hasPear = "pear" in fruits // false
// 移除元素
val removed = fruits.remove("banana") // true
// 集合运算
val tropicalFruits = setOf("mango", "pineapple", "kiwi")
val commonFruits = fruits intersect tropicalFruits
val allFruits = fruits union tropicalFruits
val difference = fruits - tropicalFruits
println("通用水果: $commonFruits")
println("所有水果: $allFruits")
println("差集: $difference")
}
}IMPORTANT
Set 的 add() 方法返回值很重要:
- 在业务逻辑中,你可以根据返回值判断元素是否真正被添加
- 这对于统计、日志记录等场景非常有用
性能考量与选择
kotlin
// 不同 Set 实现的性能特点
class SetPerformanceGuide {
// HashSet - 最常用,平均 O(1) 查找
val hashSet = hashSetOf<String>()
// LinkedHashSet - 保持插入顺序,略慢于 HashSet
val linkedHashSet = linkedSetOf<String>()
// TreeSet - 自动排序,O(log n) 查找
val treeSet = sortedSetOf<String>()
fun performanceComparison() {
val data = (1..100000).map { "item$it" }
// HashSet - 适合大多数场景
val start1 = System.currentTimeMillis()
val hashSet = data.toHashSet()
val time1 = System.currentTimeMillis() - start1
// TreeSet - 需要排序时使用
val start2 = System.currentTimeMillis()
val treeSet = data.toSortedSet()
val time2 = System.currentTimeMillis() - start2
println("HashSet 耗时: ${time1}ms")
println("TreeSet 耗时: ${time2}ms")
}
}常见陷阱与注意事项
陷阱一:可变对象作为 Set 元素
危险操作
当 Set 中存储的是可变对象时,修改对象属性可能导致 Set 的唯一性被破坏!
kotlin
data class User(var name: String, var email: String)
fun demonstrateMutableObjectTrap() {
val users = mutableSetOf<User>()
val user1 = User("Alice", "alice@example.com")
val user2 = User("Bob", "bob@example.com")
users.add(user1)
users.add(user2)
println("初始大小: ${users.size}") // 2
// 危险操作:修改已在 Set 中的对象
user1.name = "Bob"
user1.email = "bob@example.com"
// 现在 user1 和 user2 在逻辑上相同,但 Set 不知道!
println("修改后大小: ${users.size}") // 仍然是 2,但逻辑上应该是 1
}CAUTION
最佳实践:在 Set 中使用不可变对象(如 data class 的不可变属性),或者确保对象的 hashCode 和 equals 不依赖于可变属性。
陷阱二:忽略 equals 和 hashCode
kotlin
// 错误示例:没有正确实现 equals 和 hashCode
class BadUser(val name: String) {
// 没有重写 equals 和 hashCode - 危险!
}
// 正确示例:使用 data class 或手动实现
data class GoodUser(val name: String)
// data class 自动生成正确的 equals 和 hashCode
fun demonstrateEqualsHashCodeImportance() {
val badUsers = mutableSetOf<BadUser>()
badUsers.add(BadUser("Alice"))
badUsers.add(BadUser("Alice")) // 会被添加!因为是不同的对象实例
println("BadUser Set 大小: ${badUsers.size}") // 2 - 错误!
val goodUsers = mutableSetOf<GoodUser>()
goodUsers.add(GoodUser("Alice"))
goodUsers.add(GoodUser("Alice")) // 不会被添加,因为内容相同
println("GoodUser Set 大小: ${goodUsers.size}") // 1 - 正确!
}高级应用:Set 在微服务中的妙用
分布式去重场景
kotlin
@Service
class DistributedDeduplicationService(
private val redisTemplate: RedisTemplate<String, String>
) {
fun processUniqueEvents(events: List<Event>): Set<Event> {
val processedEventIds = mutableSetOf<String>()
val uniqueEvents = mutableSetOf<Event>()
events.forEach { event ->
val eventKey = "processed:${event.id}"
// 使用 Redis SET 命令进行分布式去重
val wasNew = redisTemplate.opsForSet()
.add("global:processed:events", event.id) ?: 0 > 0
if (wasNew) {
uniqueEvents.add(event)
processedEventIds.add(event.id)
}
}
return uniqueEvents
}
}API 限流中的应用
kotlin
@Component
class RateLimiter {
private val requestTokens = mutableMapOf<String, MutableSet<Long>>()
fun isAllowed(clientId: String, windowSizeMs: Long, maxRequests: Int): Boolean {
val now = System.currentTimeMillis()
val windowStart = now - windowSizeMs
val clientTokens = requestTokens.computeIfAbsent(clientId) {
mutableSetOf()
}
// 清理过期的时间戳
clientTokens.removeIf { it < windowStart }
return if (clientTokens.size < maxRequests) {
clientTokens.add(now)
true
} else {
false
}
}
}总结与展望 🚀
Set 集合看似简单,但它解决的"去重"问题在实际开发中无处不在:
- 🎯 问题追踪:避免重复问题
- 👥 用户管理:唯一的用户标签、权限
- 🔑 缓存管理:唯一的缓存键
- 📊 数据统计:去重统计
- 🌐 分布式系统:全局去重
TIP
何时选择 Set?
- 当你需要确保元素唯一性时
- 当你需要高效的存在性检查时
- 当你需要进行集合运算(交集、并集、差集)时
Set 不仅仅是一个数据结构,更是一种表达业务意图的方式。当你使用 Set 时,你就在向代码的读者明确传达:"这里的数据应该是唯一的"。
在 Kotlin + SpringBoot 的世界里,善用 Set 能让你的代码更加健壮、语义更加清晰。记住:选择正确的数据结构,往往比优化算法更重要! ✨