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
实际上只比较了 firstname
和 lastname
,两者相同自然返回 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