全栈范

互联网 & 技术 & 产品 & 阅读 & 生活

0%

踩坑记录:Kotlin data class 在 Map 和 Set 中无法读取的问题

踩坑记录:Kotlin data class 在 Map 和 Set 中无法读取的问题

Kotlin 的 data class 自动覆写了 equals()hashCode() 两个方法。虽然有些时候可以给用户带来便利,但是如果使用不慎就会出现一些匪夷所思的问题,比如这里遇到的一个问题是,将一个对象作为键放到 Map (或者 Set)之后,修改了对象的一些属性之后再从 Map (或者 Set)用它作为 key 读取的时候返回的数据是 null.

下面一段示例代码,取出来的结果是 null:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fun main(vararg args: String) {
val sample = Sample(0)
val map = mutableMapOf<Sample, String>()
map[sample] = "1"
// 可以拿到 value
println(map[sample])
// 修改了对象的一些属性
sample.id = 2
// 拿不到 value 了
println(map[sample])
// 但是对象还是同一个对象
println(map.keys.first() == sample)
}

data class Sample(var id: Int) // 肇事者

这个程序的输出结果是,

1
2
3
1
null
true

从 Kotlin 反编译的结果来看,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public final class Sample {
private int id;

public final int getId() {
return this.id;
}

public final void setId(int var1) {
this.id = var1;
}

public Sample(int id) {
this.id = id;
}

// ...

public int hashCode() {
return this.id;
}

public boolean equals(Object var1) {
if (this != var1) {
if (var1 instanceof Sample) {
Sample var2 = (Sample)var1;
if (this.id == var2.id) {
return true;
}
}

return false;
} else {
return true;
}
}
}

也就是说 data class 覆写了 hashCode()equals() 方法,并且内部使用所有字段参与两个方法的计算,所以如果任意一个字段发生变化,前后两个 hashCode() 将会发生变化,而 HashMap 的 get() 方法先通过哈希码进行散列,只有出现哈希冲突的时候才使用 equals() 进行计算。虽然存储在 Map 中的对象是同一个,但是因为前后的哈希值发生变化,所以前后散列的位置是不同的。因此修改了字段属性之后,再使用 get() 方法获取数据的时候,找到的坑位(position)不是之前的那个坑,所以就不会走到 eqauls() 的方法进行校验。当然,如果当数据修改了之后整个 Map 做了一次 rehash,可能会解决这个问题(当然我们不会这么做)。

当我们在做实际开发的时候,本身覆写 hashCode()equals() 方法的时候就需要考虑具体的应用场景。比如说,使用数据对象的数据库索引或者某些业务 ID 作为对象的唯一标识,而不是所有的字段进行计算,而 data class 默认采用使用所有字段参与计算,并不一定满足我们业务场景的需求。

所以,结论就是,data class 看上去简洁但是有欺骗性,它帮你覆写了一些方法,大部分应用场景下可能不存在问题,但是一旦出现问题就是匪夷所思的。既然我们无法保证 data class 是否会应用到诸如作为 Map 和 Set 的 key 的场景,所以,最好的避免方法就是,不要在项目中使用 data class!


-----本文结束 感谢阅读---------

欢迎关注我的其它发布渠道