目录

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 反序列化、混淆等场景里反复踩坑。