你可能不知道的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 简洁高效的优势,规避隐形成本。

相关推荐
Haha_bj1 天前
七、Kotlin——扩展(Extensions)
android·kotlin
urkay-1 天前
Android getDrawingCache 过时废弃
android·java·开发语言·kotlin·iphone·androidx
用户69371750013841 天前
24.Kotlin 继承:调用超类实现 (super)
android·后端·kotlin
alexhilton1 天前
借助RemoteCompose开发动态化页面
android·kotlin·android jetpack
QING6182 天前
Jetpack Compose Brush API 简单使用实战 —— 新手指南
android·kotlin·android jetpack
QING6182 天前
Jetpack Compose Brush API 详解 —— 新手指南
android·kotlin·android jetpack
鹿里噜哩2 天前
Spring Authorization Server 打造认证中心(二)自定义数据库表
spring boot·后端·kotlin
用户69371750013842 天前
23.Kotlin 继承:继承的细节:覆盖方法与属性
android·后端·kotlin
Haha_bj2 天前
五、Kotlin——条件控制、循环控制
android·kotlin
Kapaseker2 天前
不卖课,纯干货!Android分层你知多少?
android·kotlin