1. 概述
本文将深入探讨 Kotlin 中对象比较的几种方式。
===
及其反向操作符 !==
用于引用身份比较(referential identity)。也就是说,只有当两个变量指向堆内存中的同一个对象实例时,结果才为 true
。但在实际开发中,我们更关心的是两个对象的内容是否相等,即所谓的“结构相等性(structural equality)”。
✅ 所以重点来了:日常使用中,我们几乎不会用 ===
做业务逻辑判断,真正常用的是 ==
和数据类(data class)带来的语义化相等判断。
下面我们就来看看 Kotlin 是如何实现结构相等的。
2. 使用 == 运算符进行相等比较
在 Kotlin 中,==
和 !=
被设计用来做“有意义”的相等判断 —— 它底层会调用对象的 equals()
方法,而不是简单地比较引用地址。
比如字符串:
val a = "Baeldung"
val b = "Baeldung"
assertTrue(a == b)
上面这段代码返回 true
,这符合预期。但这背后其实是 JVM 的 字符串常量池(String Pool) 在起作用。当我们写两个相同字面量的字符串时,JVM 会复用已存在的对象引用,所以它们实际上是同一个实例。
⚠️ 然而,对于自定义类,默认行为就不同了。
假设我们有这样一个类:
class Storage(private val name: String, private val capacity: Int)
然后创建两个内容完全相同的实例并比较:
val storage1 = Storage("980Pro M.2 NVMe", 1024)
val storage2 = Storage("980Pro M.2 NVMe", 1024)
assertFalse(storage1 == storage2)
❌ 结果居然是 false
!
原因很简单:虽然 ==
会调用 equals()
,但 Storage
类没有重写该方法,因此默认执行的是引用相等性检查,相当于 Java 中的 ==
行为。即使字段值一样,只要不是同一个对象,就判为不等。
这就是新手容易踩坑的地方:以为 ==
天然支持值比较,其实前提是类必须正确实现 equals()
。
3. 重写 equals() 与 hashCode()
要想让 ==
判断字段值是否一致,就必须手动重写 equals()
方法。
来看一个示例实现:
class StorageOverriddenEquals(val name: String, val capacity: Int) {
override fun equals(other: Any?): Boolean {
if (other == null) return false
if (this === other) return true
if (other !is StorageOverriddenEquals) return false
if (name != other.name || capacity != other.capacity) return false
return true
}
}
此时再比较两个同内容实例:
val storage1 = StorageOverriddenEquals("980Pro M.2 NVMe", 1024)
val storage2 = StorageOverriddenEquals("980Pro M.2 NVMe", 1024)
assertTrue(storage1 == storage2)
✅ 成功返回 true
。
⚠️ 关键提醒:必须同时重写 hashCode()
这是从 Java 继承下来的重要契约 —— 如果两个对象通过 equals()
判定相等,则它们的 hashCode()
必须相同。
否则,在使用 HashSet
、HashMap
等哈希集合时会出现严重问题:明明相等的对象却无法去重或正确查找。
修复方案是补充 hashCode()
实现:
class StorageOverriddenEqualsAndHashCode(private val name: String, private val capacity: Int) {
override fun equals(other: Any?): Boolean {
if (other == null) return false
if (this === other) return true
if (other !is StorageOverriddenEqualsAndHashCode) return false
if (name != other.name || capacity != other.capacity) return false
return true
}
override fun hashCode(): Int = name.hashCode() * 31 + capacity
}
📌 小贴士:
- 使用质数(如 31)是为了减少哈希冲突。
- 手动计算
hashCode
容易出错,建议借助 IDE 自动生成或直接使用 data class。
4. 使用数据类(Data Class)
Kotlin 提供了一个非常优雅的解决方案:data class。
只需在类前加上 data
关键字,编译器就会自动为你生成:
equals()
hashCode()
toString()
copy()
- 以及基于主构造函数参数的
componentN()
函数
示例:
data class StorageDataClass(val name: String, val capacity: Int)
使用效果:
val storage1 = StorageDataClass("980Pro M.2 NVMe", 1024)
val storage2 = StorageDataClass("980Pro M.2 NVMe", 1024)
assertTrue(storage1 == storage2)
✅ 输出为 true
,无需任何样板代码。
💡 注意事项:
- 只有主构造函数中的属性才会参与
equals
和hashCode
计算 - 属性需用
val
或var
声明,否则不会被纳入生成逻辑 - 如果你需要自定义比较逻辑(比如忽略某个字段),仍可手动重写
equals()
,但要注意同步维护一致性
5. 总结
方式 | 是否推荐 | 场景 |
---|---|---|
=== / !== |
❌ 仅特殊场景 | 引用身份比较(极少用于业务) |
默认 == |
⚠️ 不够用 | 自定义类未重写 equals 时不可靠 |
手动重写 equals() & hashCode() |
✅ 可行但繁琐 | 需精细控制比较逻辑且不能用 data class |
data class + == |
✅✅✅ 强烈推荐 | 绝大多数需要值比较的场景 |
📌 核心结论:
在 Kotlin 中进行对象结构比较时,优先考虑使用 data class。它不仅语法简洁,还能避免因忘记重写
hashCode()
导致的潜在 Bug。
所有示例代码均可在 GitHub 获取:https://github.com/Baeldung/kotlin-tutorials/tree/master/core-kotlin-modules/core-kotlin-lang-oop-3