最新JDK8HashMamp實現過程源碼分析(二)

版權聲明:本文爲博主原創文章,未經博主允許不得轉載。 https://blog.csdn.net/youngogo/article/details/81281959

我們繼續進行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的特點是什麼?以及它的使用場景

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

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章