我們繼續進行HashMap的源碼實現分析
1、hash函數的實現,以及爲什麼table必須是2的N次方
在get和put的過程中,計算下標時,先對hashCode進行hash操作,然後再通過hash值進一步計算下標,如下圖所示:
HashMap源碼是這樣實現的
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
首先hashCode默認是將對象的存儲地址進行映射,在你沒有重寫hashCode方法時。此處的實現是,將結果的高16位和低16位異或操作,這就是HashMap的高明之處。先看個例子,一個十進制數32768(二進制1000 0000 0000 0000),經過上述公式運算之後的結果是35080(二進制1000 1001 0000 1000)。看出來了嗎?或許這樣還看不出什麼,再舉個數字61440(二進制1111 0000 0000 0000),運算結果是65263(二進制1111 1110 1110 1111),現在應該很明顯了,它的目的是讓“1”變的均勻一點,散列的本意就是要儘量均勻分佈。
//元素在table中的位置需要進一步計算
table[(n - 1) & hash]
接下來就是我們要說的table的長度爲什麼必須是2的n次方:
1、保證爲2次冪,n-1的二進制表示形式肯定是:00000.....1111,這樣(n-1)&hash的結果肯定落在table區間裏面,這是前提。
2、充分利用第一步進行異或的結果,是的table中的元素更加分散,減小了衝突。
3、便是在resize()時,使得擴展的數組更加分散,接下來詳細分析resize實現過程。
2、HashMap的resize()的實現
當put時,如果發現目前的bucket佔用程度已經超過了Load Factor所希望的比例,那麼就會發生resize。在resize的過程,簡單的說就是把bucket擴充爲2倍,之後重新計算index,把節點再放到新的bucket中。當超過限制的時候會resize,然而又因爲我們使用的是2次冪的擴展(指長度擴爲原來2倍),所以,元素的位置要麼是在原位置,要麼是在原位置再移動2次冪的位置。
怎麼理解呢?例如我們從16擴展爲32時,具體的變化如下所示:
因此元素在重新計算hash之後,因爲n變爲2倍,那麼n-1的mask範圍在高位多1bit(紅色),因此新的index就會發生這樣的變化:
因此,我們在擴充HashMap的時候,不需要重新計算hash,只需要看看原來的hash值新增的那個bit是1還是0就好了,是0的話索引沒變,是1的話索引變成“原索引+oldCap”。可以看看下圖爲16擴充爲32的resize示意圖:
這個設計確實非常的巧妙,既省去了重新計算hash值的時間,而且同時,由於新增的1bit是0還是1可以認爲是隨機的,因此resize的過程,均勻的把之前的衝突的節點分散到新的bucket了。
接下來是源碼解析:
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
// 超過最大值就不再擴充了,就只好隨你碰撞去吧
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 沒超過最大值,就擴充爲原來的2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 計算新的resize上限
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
// 把每個bucket都移動到新的buckets中
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
// 原索引
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
// 原索引+oldCap
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 原索引放到bucket裏
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 原索引+oldCap放到bucket裏
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
3、重寫了equals方法爲什麼要重寫hashCode方法
我們先來段網上down下來的例子:
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
class People{
private String name;
private int age;
public People(String name,int age) {
this.name = name;
this.age = age;
}
public void setAge(int age){
this.age = age;
}
@Override
public boolean equals(Object obj) {
// TODO Auto-generated method stub
return this.name.equals(((People)obj).name) && this.age== ((People)obj).age;
}
}
public class Main {
public static void main(String[] args) {
People p1 = new People("Jack", 12);
System.out.println(p1.hashCode());
HashMap<People, Integer> hashMap = new HashMap<People, Integer>();
hashMap.put(p1, 1);
System.out.println(hashMap.get(new People("Jack", 12)));
}
}
在這裏我只重寫了equals方法,也就說如果兩個People對象,如果它的姓名和年齡相等,則認爲是同一個人。這段代碼本來的意願是想這段代碼輸出結果爲“1”,但是事實上它輸出的是“null”。爲什麼呢?原因就在於重寫equals方法的同時忘記重寫hashCode方法。
雖然通過重寫equals方法使得邏輯上姓名和年齡相同的兩個對象被判定爲相等的對象(跟String類類似),但是要知道默認情況下,hashCode方法是將對象的存儲地址進行映射。那麼上述代碼的輸出結果爲“null”就不足爲奇了。原因很簡單,p1指向的對象和System.out.println(hashMap.get(new People("Jack", 12)));這句中的new People("Jack", 12)生成的是兩個對象,它們的存儲地址肯定不同。
@Override
public int hashCode() {
// TODO Auto-generated method stub
return name.hashCode()*37+age;
}
因此如果想上述代碼輸出結果爲“1”,很簡單,只需要重寫hashCode方法,讓equals方法和hashCode方法始終在邏輯上保持一致性。
- 通過以上的舉例,我想基本應該也就理解了思想,小編在理解這個知識點的時候,有個想法
Person p1 =new Person();
p1.setAge(22);
p1.setName("xiaoming");
Person p2 = new Person();
p2.setAge(22);
p2.setName("xiaoming");
map.put(p1, 1);
map.put(p2, 2);
System.out.println("這是兩個對象比較");//直接調用的是Object的equals()方法
System.out.println(p1.equals(p2));
Integer integer = map.get(p1);
在person方法裏面我重寫了equals方法,但是沒有重寫hashCode方法, 我可以通過p1和p2實例取出各自的信息啊,並且兩者沒有實現覆蓋(因爲兩者的hashcode值不同)。但是這樣的做法,是沒有實際意義的,就像剛纔做的那樣,你如果new一個一模一樣的person確取不出來值,他們在邏輯上是一模一樣的含義。
4、對於開始我們提出問題的回答
1)HashMap的特點是什麼?以及它的使用場景
2)HashMap的工作原理是什麼?
通過hash的方法,通過put和get存儲和獲取對象。存儲對象時,我們將K/V傳給put方法時,它調用hashCode計算hash從而得到bucket位置,進一步存儲,HashMap會根據當前bucket的佔用情況自動調整容量(超過Load Facotr則resize爲原來的2倍)。獲取對象時,我們將K傳給get,它調用hashCode計算hash從而得到bucket位置,並進一步調用equals()方法確定鍵值對。如果發生碰撞的時候,Hashmap通過鏈表將產生碰撞衝突的元素組織起來,在Java 8中,如果一個bucket中碰撞衝突的元素超過某個限制(默認是8),則使用紅黑樹來替換鏈表,從而提高速度。
3)equals和hashCode都有什麼作用?
通過對key的hashCode()進行hashing,並計算下標( n-1 & hash),從而獲得buckets的位置。如果產生碰撞,則利用key.equals()方法去鏈表或樹中去查找對應的節點
4)你知道hash的實現嗎?爲什麼要這樣實現?
在Java 1.8的實現中,是通過hashCode()的高16位異或低16位實現的:(h = k.hashCode()) ^ (h >>> 16)
,主要是從速度、功效、質量來考慮的,這麼做可以在bucket的n比較小的時候,也能保證考慮到高低bit都參與到hash的計算中,同時不會有太大的開銷。
歡迎大家留言交流,小編qq:1298364867