1. 概述

Kotlin 的 data class 是专为数据承载设计的语法糖,能极大简化 POJO 类的定义。它自动生成 toString()equals()hashCode()copy() 等常用方法,提升开发效率。

但如果你对 equals() 的实现机制理解不到位,很容易踩坑。本文通过两个典型场景,深入剖析 data class 的 equals() 行为,并提供解决方案。

✅ 所有示例代码均基于单元测试验证,确保结论准确可靠。


2. Kotlin data class 简要回顾

data class 的核心用途是封装数据,例如:

data class Person(val firstname: String, val lastname: String)

关键限制包括:

  • ❌ 必须有非空的主构造函数
  • ❌ 不能被继承(即不能用 open 修饰)

✅ Kotlin 会自动为 data class 生成以下方法:

  • toString()
  • equals()
  • hashCode()
  • copy()

这些方法仅基于主构造函数中声明的属性进行生成。这一点至关重要,后续的坑都源于此。


3. 数据类 equals 坑位 #1:非主构造函数属性被忽略

3.1 问题引入

假设我们想扩展 Person 类,加入出生日期字段 dateOfBirth,但出于某些原因未将其放入主构造函数:

data class PersonV1(val firstname: String, val lastname: String) {
    lateinit var dateOfBirth: LocalDate
}

接着创建两个实例:

val p1 = PersonV1("Amanda", "Smith").also { it.dateOfBirth = LocalDate.of(1992, 8, 8) }
val p2 = PersonV1("Amanda", "Smith").also { it.dateOfBirth = LocalDate.of(1976, 11, 18) }

按常理,两人出生年份不同,显然不应相等。但运行以下断言:

assertTrue { p1 == p2 }

⚠️ 测试竟然通过了!说明 p1 == p2 成立。

这意味着如果将这两个对象作为 HashMap 的 key,后者会覆盖前者——典型的逻辑 bug 温床。

3.2 根本原因与解决方案

原因分析

data class 自动生成的 equals()hashCode() 只考虑主构造函数中的属性dateOfBirth 是后期添加的成员变量,不在主构造函数中,因此完全不会参与比较。

也就是说,p1 == p2 实际上只比较了 firstnamelastname,两者相同自然返回 true。

解决方案

有两种方式修复:

✅ 推荐做法:将属性移入主构造函数

data class PersonV2(
    val firstname: String,
    val lastname: String,
    val dateOfBirth: LocalDate
)

这样所有字段都会纳入 equals() 计算,无需手动干预。

⚠️ 备选方案:重写 equals 和 hashCode

若因兼容性等原因无法修改构造函数,可手动重写:

data class PersonV2(val firstname: String, val lastname: String) {
    lateinit var dateOfBirth: LocalDate

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is PersonV2) return false

        if (firstname != other.firstname) return false
        if (lastname != other.lastname) return false
        if (dateOfBirth != other.dateOfBirth) return false

        return true
    }

    override fun hashCode(): Int {
        var result = firstname.hashCode()
        result = 31 * result + lastname.hashCode()
        result = 31 * result + dateOfBirth.hashCode()
        return result
    }
}

测试验证:

val p1 = PersonV2("Amanda", "Smith").also { it.dateOfBirth = LocalDate.of(1992, 8, 8) }
val p2 = PersonV2("Amanda", "Smith").also { it.dateOfBirth = LocalDate.of(1976, 11, 18) }
assertFalse { p1 == p2 } // ✅ 正确判断为不相等

💡 提示:手动实现时注意 == 在 Kotlin 中是结构比较(调用 equals),引用比较使用 ===


4. 数据类 equals 坑位 #2:数组比较陷阱

4.1 问题引入

再看一个看似“合规”的 data class:

data class BaeldungString(
    val value: String,
    val chars: CharArray,
)

所有属性都在主构造函数中,应该没问题了吧?创建两个内容相同的实例:

val s1 = BaeldungString("Amanda", charArrayOf('A', 'm', 'a', 'n', 'd', 'a'))
val s2 = BaeldungString("Amanda", charArrayOf('A', 'm', 'a', 'n', 'd', 'a'))

直觉上 s1 == s2 应该为 true,但实际结果却是:

assertFalse { s1 == s2 } // ⚠️ 测试通过,说明两者不等!

这显然不符合预期。

4.2 数组比较机制解析与修复

根本原因

虽然 chars 在主构造函数中,会被纳入 equals() 比较,但 Kotlin 对数组的 == 操作符默认是引用比较,而非内容比较。

验证如下:

val list1 = listOf("one", "two", "three", "four")
val list2 = listOf("one", "two", "three", "four")

val array1 = arrayOf("one", "two", "three", "four")
val array2 = arrayOf("one", "two", "three", "four")

assertTrue { list1 == list2 }     // ✅ List 支持结构比较
assertFalse { array1 == array2 }  // ⚠️ Array 不支持,这是引用比较

要实现数组内容比较,应使用 contentEquals()

assertTrue { array1 contentEquals array2 } // ✅ 内容相等

同理,hashCode() 也需使用 contentHashCode()

解决方案

✅ 推荐做法:优先使用 List 替代 Array

data class BaeldungStringV2(
    val value: String,
    val chars: List<Char>
)

List 天然支持结构比较,无需额外处理。

⚠️ 若必须使用 Array,则重写 equals/hashCode

data class BaeldungStringV2(val value: String, val chars: CharArray) {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is BaeldungStringV2) return false

        if (value != other.value) return false
        if (!chars.contentEquals(other.chars)) return false // 使用 contentEquals

        return true
    }

    override fun hashCode(): Int {
        var result = value.hashCode()
        result = 31 * result + chars.contentHashCode() // 使用 contentHashCode
        return result
    }
}

测试验证:

val s1 = BaeldungStringV2("Amanda", charArrayOf('A', 'm', 'a', 'n', 'd', 'a'))
val s2 = BaeldungStringV2("Amanda", charArrayOf('A', 'm', 'a', 'n', 'd', 'a'))

assertTrue { s1 == s2 } // ✅ 修复成功

📌 总结:在 data class 中尽量避免使用 Array,优先选择 List、Set 等集合类型


5. 总结

本文通过两个实战案例揭示了 Kotlin data class equals() 方法的常见陷阱:

问题 原因 建议
非主构造函数属性不参与比较 自动生成方法仅基于主构造函数参数 属性尽量放在主构造函数中
Array 比较是引用而非内容 == 对 Array 是引用比较 优先使用 List,或重写时用 contentEquals

✅ 最佳实践建议:

  • data class 所有参与业务逻辑比较的字段都应放在主构造函数中
  • 避免在 data class 中使用 Array,改用 List
  • 如需打破规则,务必手动重写 equals()hashCode(),并注意数组的特殊处理

源码已上传至 GitHub:Baeldung/kotlin-tutorials


原始标题:Data Class’s equals() Method