你可能不知道的Kotlin Data Class陷阱

Kotlin 中的数据类主要用于存储数据。对于每个数据类,编译器会自动生成额外的成员函数,使你能够将实例打印为可读输出、比较实例、复制实例等。

Kotlin 复制代码
data class Actor(val name: String, val age: Int)

用该例子为例,我们看一下 Kotlin 编译器会为我们生成哪些内容:

  • equals()hashCode() 方法对。
  • 格式为 "Actor (name=John, age=42)" 的 toString() 方法。
  • 与属性声明顺序相对应的 componentN() 函数。
  • 与属性声明顺序相对应的 getN() 函数。
  • copy() 函数。

陷阱1:消失的属性

如果我们将一个属性放到类的内部呢?

Kotlin 复制代码
data class Actor(val name: String) { 
    var age: Int = 0
}

我们做个简单的测试:

Kotlin 复制代码
val young_herry = Actor("Henry")
young_herry.age = 28
val kid_herry = Actor("Henry")
kid_herry.age = 14
    
println(young_herry == kid_herry)

// Output
// true

没错,虽然我们是两个实例,name 一样但是 age 不一样,但是它们依然相等。

原因就是:

编译器仅为主构造函数中定义的属性自动生成函数。

上面例子,编译器只会为 name 属性生成对应的 toString()equals()hashCode()copy(),并且只有一个组件函数 component1()age 属性是在类内部中声明的,因此不包括在内。

陷阱2:拷贝

Kotlin 复制代码
data class Book(val name:String, val tags:MutableList<String>)

我们定义一个 Book,里面包含 nametags 两个属性。现在,我们测试一下拷贝,也就是 copy

Kotlin 复制代码
val math = Book("math", mutableListOf("hard", "beauty"))
val history = math.copy(name = "history")
history.tags.add("easy")
history.tags.add("thoughtful")

println(math)
println(history)

// Output
// Book(name=math, tags=[hard, beauty, easy, thoughtful])
// Book(name=history, tags=[hard, beauty, easy, thoughtful])

我们发现,mathhistory 有着相同的标签,在 history 上添加的标签竟然也影响了 math

之所以会这样,原因是:

copy() 方法仅复制主构造函数属性的引用,俗称"浅拷贝"。

如果我们要实现深拷贝,需手动实现 copy

Kotlin 复制代码
data class Book(val name:String, val tags:MutableList<String>) {
    fun deepCopy(name: String = this.name, tags: MutableList<String> = this.tags.toMutableList()): Book {
        return Book(name, tags)
    }
}

这里有个小注意事项:

data classcopy 方法不可直接覆盖。

后续在使用深拷贝的时候,需要使用 deepCopy

陷阱3:var

我们将第一个陷阱的代码稍作改动:

Kotlin 复制代码
data class Actor(val name: String, var age: Int)

现在,我们可以创建我们演员,去演一部超级英雄电影了:

Kotlin 复制代码
val henry = Actor("Henry", 28)
val ben = Actor("Ben", 58)
val tom = Actor("Tom", 24)

val heros = hashMapOf(
    henry to "SuperMan",
    ben to "BatMan",
    tom to "SpiderMan",
)

henry.age += 2

println(heros[henry])

// Output
// null

What,亨利长大了两岁就不是超人了吗?

稍等,我来慢慢解释一下这个问题。

我们看下在改变 age 之后,两个实例的 hashCode() 是否相等。

Kotlin 复制代码
val henry = Actor("Henry", 28)
println(henry.hashCode())

henry.age += 2
println(henry.hashCode())

// Output
// -2137002460
// -2137002458

我们发现,在改变了 age 之后,虽然实例是同一个,但是他们的 hashCode() 返回值却不一样了。原因也很简单,我们看下反编译的 Actor 代码:

Java 复制代码
public int hashCode() {
   int result = this.name.hashCode();
   result = result * 31 + Integer.hashCode(this.age);
   return result;
}

没错,当我们每次调用 hashCode() 的时候,都会重新计算一次 HASH 值。那么,一切就能解释的通了!HashMap 本身在存储键值对的时候,需要使用 HASH 进行键的匹配,这就造成了,在改变 henryage 之后,两次 HASH 值不一样了,从而无法找到原来的那个 HASH 值了。

普通的类就没这个问题了吗?

还真没有!因为普通的类编译器没有给实现 hashCode()equals()(除非你自己实现了),用的是基类也就是 Object 的实现,而 Object 的实现是和内存地址有关系的,也就是说,默认情况下,同一个实例的 HASH 是一样的。

当我们去掉 data,把 data class 变成普通的 class 的时候:

Kotlin 复制代码
class Actor(val name: String, var age: Int)
Kotlin 复制代码
val henry = Actor("Henry", 28)
println(henry.hashCode())

henry.age += 2
println(henry.hashCode())

// Output
// 824318946
// 824318946

亨利长大了两岁,依然是超人!

谨慎在 data class 的主构造函数中使用 var(最好不用)。

陷阱4:数组

我们对 Book 稍作更改:

Kotlin 复制代码
data class Book(val name:String, val tags:Array<String>)
Kotlin 复制代码
val math_1 = Book("math", arrayOf("hard", "beauty"))
val math_2 = Book("math", arrayOf("hard", "beauty"))

println(math_1 == math_2)

// Output
// false

从数据类的使用上来讲,equals() 就应该判断数据是否一致,但是这里却发现,两个相同内容的数组在判定上确实不相等的。

出现这种情况是因为 Kotlin (以及 Java )中的数组并没有重写 equals()hashCode() 方法来比较数组的内容。

相比之下,诸如 ListSetMap 这样的集合在Kotlin和Java中都正确地重写了这些方法,以便从结构上比较内容。 Array 使用引用相等性------只有当两个数组在内存中是同一个实例时,它们才被视为相等。 因此,当涉及到数组时,即使看起来相同的对象也会被视为不同的对象,即不相等。

如果你使用 IntelliJ 编译器的话,它会给我们提示:

Property with 'Array' type in a 'data' class: it is recommended to override 'equals()' and 'hashCode()'

如果你的 data class 包含 Array 的话,请重新实现 equals()hashCode() 方法。

总结

Kotlin 数据类虽便捷,但稍有不慎,可能掉入陷阱。有一条核心原则大家一定要记住:

数据类的核心设计原则是不可变性

希望本文能够帮到你,发挥 data class 简洁高效的优势,规避隐形成本。

相关推荐
前行的小黑炭3 小时前
Android LiveData源码分析:为什么他刷新数据比Handler好,能更节省资源,解决内存泄漏的隐患;
android·kotlin·android jetpack
Hy行者勇哥5 小时前
华为云Astro大屏从iotda影子设备抽取数据做设备运行状态的大屏实施步骤
android·kotlin·华为云
tangweiguo0305198717 小时前
Android Kotlin ViewModel 错误处理:最佳 Toast 提示方案详解
android·kotlin
人生游戏牛马NPC1号21 小时前
学习Android(五)玩安卓项目实战
android·kotlin
百锦再1 天前
Android Studio 中使用 SQLite 数据库开发完整指南(Kotlin版本)
android·xml·学习·sqlite·kotlin·android studio·数据库开发
zhangphil1 天前
Kotlin await等待多个异步任务都完成后才进行下一步操作
kotlin
_一条咸鱼_1 天前
揭秘Android View布局底层逻辑:万字源码深度剖析与实战解析
android·面试·kotlin
程序员江同学2 天前
Kotlin 技术月报 | 2025 年 4 月
android·kotlin
用户3031057186852 天前
Kotlin协程的介绍
kotlin