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() 必须相同

否则,在使用 HashSetHashMap 等哈希集合时会出现严重问题:明明相等的对象却无法去重或正确查找。

修复方案是补充 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,无需任何样板代码。

💡 注意事项:

  • 只有主构造函数中的属性才会参与 equalshashCode 计算
  • 属性需用 valvar 声明,否则不会被纳入生成逻辑
  • 如果你需要自定义比较逻辑(比如忽略某个字段),仍可手动重写 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


原始标题:Comparing Objects in Kotlin