Kotlin数据类equals详解:为什么book1 == book3会返回true?
在Kotlin开发中,数据类(data class)是我们存储数据的首选,它自带的equals()、hashCode()等方法能极大简化代码。但很多新手会有一个疑问:明明是两个不同的对象(book1和book3),为什么用==判断会返回true?今天就彻底讲透这个核心知识点,结合实例拆解,帮你避开踩坑。
这也是很多新手的入门场景:
kotlin
// 定义Book数据类
data class Book(
val id: Int,
val title: String,
val author: String,
val price: Double,
val isInStock: Boolean = true
)
fun main() {
// 创建两个属性完全相同的实例
val book1 = Book(1, "Kotlin 从入门到实战", "张三", 59.9)
val book3 = Book(1, "Kotlin 从入门到实战", "张三", 59.9)
// 输出结果:true
println("book1 == book3 ? ${book1 == book3}")
// 输出结果:false
println("book1 === book3 ? ${book1 === book3}")
}
核心疑问:book1和book3是两个独立创建的对象,占用不同的内存空间,为什么==判断会相等?答案就藏在Kotlin数据类的自动生成机制里。
一、先分清两个核心概念:== 和 ===
在Kotlin中,判断对象"相等"有两种完全不同的方式,这是理解本文问题的基础,也是和Java相等性判断的核心区别,新手一定要记牢:
| 运算符 | 判断逻辑(核心) | 通俗理解 |
|---|---|---|
| == | 结构相等(调用equals()方法),判断两个对象的内容/属性是否完全相同 | 两个人长得一模一样,不管是不是同一个人 |
| === | 引用相等,判断两个变量是否指向内存中同一个对象 | 两个人是同一个人,拥有同一个身份证(内存地址) |
补充说明:Kotlin会自动对==做空安全转换,避免空指针异常,等价于 a?.equals(b) ?: (b === null),这也是比Java更友好的设计点之一。
二、关键原因:数据类自动重写了equals()方法
我们先明确一个前提:Kotlin中所有类都继承自根类Any,Any类中的默认equals()方法,实现的是"引用相等"(和=效果一样)。也就是说,**普通类(不加data关键字)**的,判断的是内存地址,和===没有区别。
但**数据类(data class)**不一样------Kotlin编译器会自动为数据类重写equals()方法,并重写对应的hashCode()方法(两者必须同时重写,保证一致性),其重写逻辑完全围绕"数据"展开:
数据类的equals()方法,会自动比较主构造函数中的所有属性,只有当所有属性的值完全相同时,才会返回true;只要有一个属性不同,就返回false。
拆解book1 == book3的判断过程
结合我们的Book数据类,book1和book3的主构造函数属性完全一致:
-
id:都是1
-
title:都是"Kotlin 从入门到实战"
-
author:都是"张三"
-
price:都是59.9
-
isInStock:都是true(默认值)
当执行book1 == book3时,实际上是调用了Kotlin自动生成的equals()方法,该方法会逐一对上述5个属性进行比较,所有属性都相同,因此返回true。
而book1 === book3返回false,是因为两者是独立创建的对象,占用不同的内存地址,引用的不是同一个对象实例,这也符合引用相等的判断逻辑。
三、直观理解:数据类equals()的底层逻辑(模拟源码)
Kotlin编译器为Book数据类自动生成的equals()方法,大致逻辑如下(模拟Java字节码对应的Kotlin代码,便于理解),其核心是"先判引用、再判类型、最后判属性"的短路优化逻辑:
kotlin
// 编译器自动生成的equals()方法(模拟)
override fun equals(other: Any?): Boolean {
// 1. 先判断引用是否相同(同一个对象,直接返回true,提升性能)
if (this === other) return true
// 2. 判断类型是否匹配(不是Book类型,直接返回false)
if (other !is Book) return false
// 3. 逐一对主构造函数的所有属性进行比较
return id == other.id &&
title == other.title &&
author == other.author &&
price == other.price &&
isInStock == other.isInStock
}
从模拟源码能清晰看出:数据类的equals(),核心就是"比较属性值",和对象本身的内存地址无关------这也是数据类的设计初衷:专注于数据存储,我们关心的是"数据是否一致",而非"是否是同一个对象"。
四、常见误区提醒(避坑必看)
误区1:只要是数据类,所有属性都会参与equals()比较?
不是!只有主构造函数中声明的属性,才会参与equals()、hashCode()、toString()等方法的生成;如果属性声明在类体中,不会被纳入比较范围。
示例(反例):
kotlin
data class Book(
val id: Int,
val title: String
) {
// 类体中声明的属性,不参与equals()比较
var author: String = ""
}
fun main() {
val bookA = Book(1, "Kotlin入门")
bookA.author = "张三"
val bookB = Book(1, "Kotlin入门")
bookB.author = "李四"
// 输出true:author是类体属性,不参与比较
println(bookA == bookB)
}
误区2:重写equals()可以不重写hashCode()?
绝对不可以!这是Kotlin/Java的通用规范:如果两个对象的equals()返回true,它们的hashCode()必须相等;反之,hashCode()相等的对象,equals()不一定返回true。
如果只重写equals()而不重写hashCode(),会导致HashMap、HashSet等集合框架工作异常(比如无法正确去重、查找失败)。而数据类会自动同时生成这两个方法,无需我们手动处理,这也是数据类的优势之一。
误区3:数据类的equals()会比较引用类型的内部属性?
不会!数据类的equals()对引用类型(如String、List)的比较,是调用该引用类型自身的equals()方法。比如String类型本身重写了equals(),比较的是字符串内容;而如果是自定义的普通类(未重写equals()),比较的依然是引用地址。
五、总结(一句话吃透核心)
✅ book1 == book3 返回true:因为Book是数据类,equals()被自动重写,比较的是所有主构造属性的值,两者属性完全一致,因此相等;
❌ book1 === book3 返回false:因为两者是独立创建的对象,内存地址不同,引用的不是同一个实例;
✅ 核心规律:Kotlin数据类的==判"内容相同",===判"同一个对象",这是数据类最核心的特性之一,也是我们日常开发中最常用的判断方式。
最后补充:如果我们不想让数据类按默认规则比较(比如只按id判断相等),也可以手动重写equals()和hashCode()方法,覆盖编译器自动生成的逻辑,满足自定义需求。
关注我,持续分享Kotlin基础干货,帮你避开新手踩坑,高效掌握核心知识点~
六、补充对比:Java对象类、Kotlin普通类、Kotlin数据类的核心区别
结合本文核心知识点,我们进一步梳理三类常用类的区别,重点聚焦equals()、hashCode()、toString()等关键方法,帮你彻底分清不同类的使用场景,避免混淆:
| 类的类型 | equals() 逻辑 | hashCode() / toString() | 核心特性与使用场景 |
|---|---|---|---|
| Java对象类(默认) | 默认比较引用地址(和Kotlin的===效果一致);需手动重写才能实现属性比较。 | hashCode() 返回对象内存地址相关值;toString() 返回"类名@哈希值",无实际业务意义,均需手动重写。 | 通用类,可承载业务逻辑;存储数据时需手动重写常用方法,代码繁琐。 |
| Kotlin普通类(不加data) | 继承自Any类,默认比较引用地址;和Java对象类默认行为完全一致,需手动重写实现属性比较。 | 默认行为和Java一致;hashCode()、toString()均不包含属性信息,需手动重写。 | 适合承载业务逻辑,无需侧重数据存储;相比Java普通类,语法更简洁(如无需写get/set)。 |
| Kotlin数据类(加data) | 自动重写,比较主构造函数所有属性;无需手动编写,即本文核心讲解的逻辑。 | 自动重写:hashCode() 与equals() 保持一致;toString() 自动拼接所有主构造属性,直观展示数据。 | 专门用于存储数据(如实体类、DTO);自动生成copy()、解构声明,大幅简化代码,是数据存储首选。 |
关键总结:三类类的核心差异,本质是"是否自动生成数据相关的常用方法"------Kotlin数据类彻底解决了"存储数据时手动重写方法"的痛点,而Java对象类、Kotlin普通类更侧重承载业务逻辑。