
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
,里面包含 name
和 tags
两个属性。现在,我们测试一下拷贝,也就是 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])
我们发现,math
和 history
有着相同的标签,在 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 class
的copy
方法不可直接覆盖。
后续在使用深拷贝的时候,需要使用 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 进行键的匹配,这就造成了,在改变 henry
的 age
之后,两次 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()
方法来比较数组的内容。
相比之下,诸如 List
、Set
和 Map
这样的集合在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
简洁高效的优势,规避隐形成本。