Skip to content

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 能让你的代码更加健壮、语义更加清晰。记住:选择正确的数据结构,往往比优化算法更重要!