这篇文章主要通过HashMap#put
的源码来聊聊为什么当将自定义的对象需要使用到Hash
类型的数据结构的时候,需要重写hashcode
和equals
。
这是在某个大厂面试的时候,感觉自己表达不够清楚,这里再总结下,希望对大家有所帮助。
额外补充一句,程序猿要晋级到工程师抑或架构师,该从底层去探究的东西还是要耐心结合源码进行分析,不是仅仅看别人的总结和理论知识点就可以了,需要自己结合源码进行理解、巩固、实践。
对于散列结构的数据类型,比如HashMap
或者HashSet
这些数据类型。若我们从业务上判定对象是表示的同一个事物,那么需要重写equals和hashcode,来保证在内存中不同的对象在业务上是一致的。
上面这话有些绕。业务上相同的但是代码层面不相同的对象,当需要hash结构进行保存的时候,就需要重写hashcode和equals方法了。
默认情况下
equals
是继承自Object
父类的,其中通过==
进行对比hashcode
也是继承自Object
父类的,通过native
方法进行hashcode
计算。public boolean equals(Object obj) {
return (this == obj);
}
public native int hashCode();
以下是JDK8的hashmap
的源码,对此我想吐槽下:写的真是精炼,能一行写的,绝不两行写,最开始看的时候,变量哪里赋值都看了半天,实际项目编码中个人还是不推荐这种精炼的写法的,代码写出来是给大部分人读的以及后续维护的,所以性能一致或者差不多的情况下 "可读性才是最重要的"。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash && // 关键1
((k = p.key) == key || (key != null && key.equals(k)))) //关键2
e = p;
//省略。。。
}}}}
上述代码是JDK8关于HashMap
的put
的底层源码,有部分省略的地方,请注意查看"关键1"和
"关键2"
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k)))) // 关键1
e = p;
其中p
表示的是数组中根据hashcode运算后找出的某个具体的数组值。
其中e
是exsiting
的缩写,表示已经存在的,该值需要在最后做判定后插入。
说回关键1处的代码:源码编写者应该是有意的通过 &&
进行分割。关于&&
和&
的区别,我们下篇文章再谈,很有意思的一个知识点。
p.hash == hash
这个很明显是判定hash值。重写hashcode的原因在这里。(k = p.key) == key
和 (key != null && key.equals(k))
,要求满足其中1个就行了。
==
直接判定内存地址,这个判定肯定同一个对象。因为==我们是没有办法重写的。equals
,这个判定是标题提到的需要重写的equals
方法。重写eqauls的原因在这里。关键代码2处的代码也是类似的思路,请读者自行理解,不再赘述。
hashcode被重写是因为保证同一个业务对象在数组寻址时,落到同一个节点和对链表第一个节点进行判定。
eqauls被重写是因为需要在链表中寻找该对象是否已经存在,来判定替换或者插入。