
在逛 programcreek 的时候,我发现了少量专注细节但价值连城的主题。比方说:Java 的 equals() 和 hashCode() 是远房亲戚吗?像这类灵魂拷问的主题,非常值得深入地研究一下。
另外,我想要告诉大家的是,研究的过程非常的有趣,就如同在迷宫里探宝一样,起初有些不知所措,但经过一番用心的摸索后,不但会找到宝藏,还会有一种茅塞顿开的感觉,非常棒。

对于绝大多数的初级程序员或者者说不重视“内功”的老鸟来说,往往停留在“知其然不知其所以然”的层面上——会用,但要说底层的原理,可就只能挠挠头双手一摊一张问号脸了。
很长一段时间内,我,沉默王二也一直处于这种层面上。但我决定改变了,由于“内功”就如同是在打地基,只有把地基打好了,才能盖起经得住考验的高楼大厦。借此机会,我就和大家一起,对“equals() 和 hashCode()”进行一次深入地研究。
equals() 和 hashCode() 是 Java 的超级祖先类 Object 定义的两个重要的方法:
public boolean equals(Object obj)public int hashCode()讲道理,单从方法的定义上来看,equals() 和 hashCode() 这两个方法之间没有任何亲戚关系,远房都够不上资格。但往深处扒拉,它们之间还真的是有千丝万缕的关系。究竟是什么关系呢?假如大家伙比较感兴趣的话,就请随我来,打怪进阶喽!
为了勾起大家的好奇心,我特意编写了下面这个示例。
public class Cmower { private String name; public Cmower(String name) { this.name = name; } @Override public boolean equals(Object obj) { if (obj == null) return false; if (!(obj instanceof Cmower)) return false; if (obj == this) return true; return this.name.equals(((Cmower) obj).name); } public static void main(String[] args) { Cmower a1 = new Cmower("沉默王二"); Cmower a2 = new Cmower("沉默王三"); Map<Cmower, Integer> m = new HashMap<Cmower, Integer>(); m.put(a1, 18); m.put(a2, 28); System.out.println(m.get(new Cmower("沉默王二"))); }}我们定义了一个 Cmower 类,它有一个字段 name;而后我们重写了 equals() 方法:
1)假如指定对象为 null,返回 false;
2)假如指定对象的类型不是 Cmower 类,返回 false;
3)假如指定对象“==”当前对象,返回 true;
4)假如指定对象的 name 和当前对象的 name 相等,返回 true;意味着只需 name 相等,两个对象就是 equals 的。
在 main 方法中,我们创立了两个 Cmower 类型的对象,name 分别为“沉默王二”和“沉默王三”,并将它们作为键放入了 HashMap 当中;按理说,只需键的 name 为“沉默王二”,程序就应该能够获取我们之前放入的 Cmower 对象。但结果却“出人预料”:
null可明明 HashMap 中放入了“沉默王二”啊,debug 也可以证实这一点。

那到底是哪里出了错呢?
开门见山地说吧,问题出在 hashCode() 身上,Cmower 类没有重写该方法。借此机会交代一下 equals() 和 hashCode() 这两个方法之间的关系吧:
1)假如两个对象需要相等(equals),那么它们必需有着相同的哈希码(hashCode);
2)但假如两个对象有着相同的哈希码,它们却不肯定相等。
这就如同在说:你和你对象要想在一起天长地久,就必需彼此相爱;但即使你和你对象彼此相爱,但却不肯定真的能在一起。
(扎心了,老铁)
HashMap 之所以能够更快地通过键获取对应的值,是由于它的键位上使用了哈希码。当我们需要从 HashMap 中获取一个值的时候,会先把键转成一个哈希码,判断值所在的位置;而后在使用“==”操作符或者者 equals() 方法比较键位能否相等,从而取出键位上的值。
可以查看一下 HashMap 类的 get() 方法的源码。
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value;}final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; if ((e = first.next) != null) { if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null;}在上例中,之所以没有从 HashMap 中取出 name 为“沉默王二”的 Cmower 对象,就是由于 put 的时候和 get 的时候两个对象的哈希码不同的起因造成的。
明白了起因之后,我们即可以对 Cmower 类进行改造,来看重写后的 hashCode() 吧。
@Overridepublic int hashCode() { return this.name.hashCode();}重写后的 hashCode() 方法体非常简单:返回 name 字段的哈希码。这样的话,put 和 get 用到的哈希码就是相同的,由于“沉默王二”的哈希码是 867758096。再次运行程序,你就会发现输出结果不再是 null 而是 18 了。
1)equals() 的作用是用来判断两个对象能否相等。
2)hashCode() 的作用是获取对象的哈希码;哈希码一般是一个整数,用来确定对象在哈希表(比方 HashMap)中的索引位置。
拿 HashMap 来说,它本质上是通过数组实现的。当我们要获取某个“值”时,实际上是要获取数组中的某个位置的元素。而数组的位置,就是通过“键”来获取的;更进一步说,是通过“键”对应的哈希码计算得到的。
3)假如两个对象需要相等(equals),那么它们必需有着相同的哈希码(hashCode);
4)但假如两个对象有着相同的哈希码,它们却不肯定相等。
来对照一下官方给出的关于 equals() 和 hashCode() 的解释:
equals(Object) method, then calling the hashCode method on each of the two objects must produce the same integer result.equals(java.lang.Object) method, then calling the hashCode method on each of the two objects must produce distinct integer results. However, the programmer should be aware that producing distinct integer results for unequal objects may improve the performance of hash tables.可能有读者会问:“肯定要同时重写 equals() 和 hashCode() 吗?”
答复当然能否定的。假如对象作为键在哈希表中,那么两个方法都要重写,由于 put 和 get 的时候需要用到哈希码和 equals() 方法;
假如对象不在哈希表中,仅用来判断能否相等,那么重写 equals() 就行了。
嘿嘿??,这下搞清楚 equals() 和 hashCode() 之间的关系了吧!
好了,读者朋友们,以上就是本文的一律内容了。能看到这里的都是最优秀的程序员,升职加薪就是你了??。原创不易,假如觉得有点用的话,请不要吝啬你手中点赞的权力——由于这将是我写作的最强动力。
