HashCode 与 Equals 小坑
目录
同事问了一句:「HashMap 的 hashCode 为什么是 0?」
我猜是空 map。果然,他用的就是一个空的 HashMap。于是顺带想到经典面试题 hashCode / equals,以及 Kotlin 的 data class 在这上面容易踩的坑,整理一下。
一、HashMap 的 hashCode 与 equals
实现可以直接看 AbstractMap 的源码:
public int hashCode() {
int h = 0;
for (Entry<K, V> entry : entrySet())
h += entry.hashCode();
return h;
}
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof Map<?, ?> m))
return false;
if (m.size() != size())
return false;
try {
for (Entry<K, V> e : entrySet()) {
K key = e.getKey();
V value = e.getValue();
if (value == null) {
if (!(m.get(key) == null && m.containsKey(key)))
return false;
} else {
if (!value.equals(m.get(key)))
return false;
}
}
} catch (ClassCastException | NullPointerException unused) {
return false;
}
return true;
}
要点:
- 空 map:没有 entry,累加结果就是 0,所以
hashCode()为 0 是正常的。 - equals:比较的是「键值对内容」,不是引用。
所以若把 HashMap 当 key 用(现实中很少这么干),两个「内容相同」的 map 会算出相同 hashCode,并且 equals 为 true,在同一个 Map<Map..., V> 里只会保留一个条目,后 put 的会覆盖先 put 的。例如:
fun main() {
val map1 = mutableMapOf<String, Any>("key1" to "value1")
val map2 = mutableMapOf<String, Any>("key1" to "value1")
val map3 = mutableMapOf<Map<String, Any>, Any>()
map3[map1] = "1"
map3[map2] = "2"
println(map3) // 只有一个 entry,map2 覆盖了 map1
}
这种写法一般不推荐,了解原理即可。
二、Kotlin Data Class 的坑
Data class 自动生成 equals / hashCode,比较的是属性值,不是对象引用。用在集合或 indexOf 时,容易和「按引用区分」的直觉不符。
2.1 用在 Set 里:按「内容」去重
例如:
data class CustomData(val data: Any)
val custom1 = CustomData("1")
val custom2 = CustomData("1")
val set = linkedSetOf<CustomData>()
set.add(custom1)
set.add(custom2)
println(set) // 只有 1 个元素:内容相同,equals 为 true,被去重
Set 认为两个实例「相等」,所以只保留一个。
2.2 用在 List 的 indexOf:按「内容」找下标
val list = mutableListOf<CustomData>()
list.add(custom1)
list.add(custom2)
list.indexOf(custom2) // 返回 0,因为 equals 比较内容,custom2 和 custom1 内容相同
indexOf 内部用 equals 比较,data class 的 equals 又是按属性来的,所以会匹配到第一个「内容相同」的元素(这里是 custom1),返回下标 0,而不是 1。若你心里想的是「按引用找第二个元素」,就会觉得结果不对。
三、总结
- 使用 HashMap 或任何依赖 equals / hashCode 的 API 时,要清楚:当前类型比较的是「引用」还是「内容」。
- Kotlin data class 默认按「内容」比较,在 Set 去重、List 的
indexOf、以及把对象当 Map 的 key 时,都会按内容生效,容易和「两个不同对象」的直觉冲突,从而出现难以排查的 bug。 - 这类问题本身不复杂,但若平时没注意 equals/hashCode 的语义,就会在 release、JSON 反序列化、混淆等场景里反复踩坑。