工作中編寫代碼的時候涉及到了重寫equals方法和hashCode方法,一直都是重寫equals方法時要重寫hashCode方法,但是一直不知道原理,現在將學習到知識記錄下來。(文章引用的JDK源碼均爲1.8版本)
先來幾個問題:
1. hashCode和equals的作用都是什麼?
2. 爲什麼需要重寫equals()方法?
3. 爲什麼重寫equals方法時需要同時重寫HashCode方法?
hashCode和equals的作用都是什麼?
hashCode的作用:hashCode的存在主要是用於查找的快捷性,如Hashtable,HashMap,HashSet等,hashCode是用來在散列存儲結構中確定對象的存儲地址的。
equals的作用:equals是用於比較兩個對象的是否相等的。
在Object類中,equals方法的源碼如下:
public boolean equals(Object obj) {
return (this == obj);
}
通過以上代碼可以看出,在Object的equals方法中是比較兩個對象的地址是否相等,地址相等才認爲是相等。
Java中的很多類都重寫了這兩個方法,例如String類,Integer Long Double等包裝類,String類的equals方法源碼如下:
public boolean equals(Object anObject) {
if (this == anObject) {
return true;// 內存地址相等,對象一定相等
}
if (anObject instanceof String) { // 類型不一致肯定認爲對象不相等
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {// 字符串長度不相等,肯定認爲對象不一致
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {// while循環, 逐個字符比較是否相等
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
爲什麼需要重寫equals()方法?
我們在定義類時,我們經常會希望兩個不同對象的某些屬性值相同時就認爲他們相同,所以我們要重寫equals()方法。舉例如下
package com.jd.real.stock.web.http.impl;
import java.util.Objects;
public class People {
/*身份證號*/
String idCard;
/*名字*/
String name;
public People() {}
public People(String idCard, String name) {
this.idCard = idCard;
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
People people = (People) o;
return Objects.equals(idCard, people.idCard);
}
@Override
public int hashCode() {
return Objects.hash(idCard);
}
public static void main(String[] args) {
People oneInFamily = new People("21042319930101666x", "小明");
People oneInCompany = new People("21042319930101666x", "黃小明");
// 雖然在家裏和在公司他們的名字不一樣, 但是通過身份證Id可以識別他們就是同一個人
System.out.println("Are they the same person?" + oneInCompany.equals(oneInFamily));
}
}
----------------------------------------------
Are they the same person?true
Process finished with exit code 0
爲什麼重寫equals方法時需要同時重寫HashCode方法?
前面講到了,hashCode是用來在散列存儲結構中確定對象的存儲地址的。既然談到散列的存儲結構,挑兩個典型的HashSet和HashMap瞭解一下原理。
HashSet的add(Objcect o)方法源碼如下:
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
{
static final long serialVersionUID = -5024744406713321676L;
private transient HashMap<E,Object> map;
public HashSet() {
map = new HashMap<>();
}
public boolean add(E e) {
// HashSet的底層實現原理實際是使用的HashMap,利用HashMap結構的Key來實現集合的去重效果
return map.put(e, PRESENT)==null;
}
}
HashMap的put源碼如下:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
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 &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);// 如果出現Hash衝突, 以鏈表的結構存儲新的Value,當前Hash地址下的老Value不會被刪除
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
通過HashSet的add源碼以及hashMap的put源碼可以看出來,HashSet底層是使用HahMap實現的。HashMap的Key當出現Hash衝突的時候會判斷當前Key和之前位置的Key是不是euqals,如果是同一個Key那麼新的value會直接覆蓋舊的Value。如果不equals,那麼會在同一個Hash桶中以鏈表的形式存儲下來。
掌握了以上知識,下面來說明爲什麼重寫equals需要同時重寫HashCode。
場景1:假如重寫了equals沒有重寫HashCode,那Object中原始的hashCode方法時比較兩個對象的內存地址是否相等。對於上述例子中,小明和黃小明顯然是同一個人,如果沒有重寫hashCode, 將小明和黃小明存儲到hashSet中時,就會發現都存入成功了(因爲兩個對象的HashCode不一致),就違反了HashSet的不可重複性。
場景2:假如重寫了HashCode,但是hashCode的返回值是固定值。則會出現equals不等,HashCode相同的情況。針對這種場景,當存入HashMap時,由於大家的HashCode相同,所以所有對象都會落入同一個哈希桶中,這樣哈希表就完全當做鏈表了,就失去了快速查找的特性了。
總結:
1、重寫equals時一定要重寫HashCode
2、如果equals不相等,HashCode一定也不相等。(否則就會出現場景2的問題)
3、如果equals相等,HashCode一定也相等。(否則就會出現場景1的問題)