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

相关推荐
androidwork10 小时前
Android Kotlin权限管理最佳实践
android·java·kotlin
nukix15 小时前
Android Studio Kotlin 中的方法添加灰色参数提示
android·kotlin·android studio
zimoyin1 天前
kotlin Android AccessibilityService 无障碍入门
android·开发语言·kotlin
_龙小鱼_2 天前
Kotlin扩展简化Android动画开发
android·开发语言·kotlin
_龙小鱼_2 天前
Kotlin 作用域函数(let、run、with、apply、also)对比
java·前端·kotlin
zhangphil2 天前
Android Coli 3 ImageView load two suit Bitmap thumb and formal,Kotlin(七)
android·kotlin
lpfasd1232 天前
Flutter与Kotlin Multiplatform(KMP)深度对比及鸿蒙生态适配解析
flutter·kotlin·harmonyos
androidwork3 天前
使用 Kotlin 和 Jetpack Compose 开发 Wear OS 应用的完整指南
android·kotlin
_龙小鱼_3 天前
Kotlin变量与数据类型详解
开发语言·微信·kotlin
androidwork3 天前
掌握 Kotlin Android 单元测试:MockK 框架深度实践指南
android·kotlin