HashMap-put源碼分析

寫在前面


個人全部文章同步更新與Github--Java-Notes,想了解JVM,HashMap源碼分析,spring相關,劍指offer題解(Java版),能夠點個star。能夠看個人github主頁,天天都在更新喲。java

邀請您跟我一同完成 reponode


HashMap 數據結構很是重要,常常被用來面試。由於它綜合了數組以及鏈表的知識,還有很是重要的hash算法,在之後的工做中也常常被用到,其中還有不少很是高效的算法。可是hashMap對於不少人來講比較困難,可能會用,可是並不清楚怎麼實現,或者不清楚他的執行邏輯。git

我就經過語句的執行以及函數的調用順序來一步步揭開 hashMap的面紗,跟着個人思路走,至少hashMap的基本邏輯就知道了,校招相關的面試基本也能答得上來github

註釋應該很是很是細了,由於我基本判斷語句以及一些不清楚的變量邏輯都進行了中文註釋面試

  • 採用 JDK 8 的源碼進行分析
  • 本人技術有限,紅黑樹部分並無進行分析,不過對於理解 HashMap 的存取過程影響不太大
  • 對於泛型K,V使用 Object代替,其餘的關鍵字好比final,transient並無寫。由於這不是重點
  • 爲了大家方便,我在截圖的時候截取了源碼的行號,大家能夠自行去查看源碼對應的位置
  • 數據類型,1.8應該使用的是 Node 命名,可是我使用的是 Entry,不過邏輯仍是1.8的邏輯

本文結構脈絡

我的理解語句以及中文註釋

存放在個人 github 上:算法

github.com/leosanqing/…spring

相似於這種格式數組

HashMap 的數據結構

數組+鏈表

爲啥採用這種方式

固然是爲了快,爲了效率數據結構

數組在知道下標以後查詢速度尤爲快,O(1)的時間複雜度函數

鏈表在增刪的時候速度很是快,找到位置後(前提),處理只須要O(1)的時間複雜度,由於不須要移動數據的位置,只須要更改指向的地址便可。可是鏈表在遍歷對比的時候很是慢,時間複雜度爲O(n),因此用來作 哈希衝突時的解決方法

因此查詢一個數據的時間複雜度爲 O(1)+O(n)。不過由於哈希算法的很是巧妙,會讓衝突儘量地均勻分佈,因此鏈通常極其短。因此後面遍歷鏈表的時間能夠忽略不計,並且在 JDK8 以後,若是衝突的鏈表長度大於 8,那麼就會轉化爲 紅黑樹,他的遍歷的時間複雜度爲O(log n)

源碼中的變量名

數組

數組的話,源碼中使用的是 table 命名,你也能夠稱之爲

Node[] table;
複製代碼

鏈表

鏈表的話,JDK 1.7中使用的是 Entry,JDK1.8採用的是 Node命名。基本同樣,只是名字不一樣,結構定義以下.

(我是按照1.7的命名, 不過其餘邏輯是1.8的)

/** * Entry 類 爲map中基本的單元 * * key 爲鍵,value 爲值 * next 是在哈希衝突時,指向的下一個 Entry * h 爲傳入的hash值,源碼中爲 hash */
static class Entry{
        Object key;
        Object value;
        Entry next;
        int h;
}
複製代碼

其餘

// 初始默認的數組容量
static final int INIT_CAPACITY = 1<<4;
//數組最大的容量,由於 數組設置爲 2的整次方倍,而 32 次方爲負數,因此最大隻能爲 1 << 30,即2的31次方
static final int MAX_CAPACITY = 1<<30;
// 默認的裝填因子
static final float DEFAULT_LOADFACTOR = 0.75f;

// table 桶中的個數--數組的大小;
int size;

// 修改次數
int modCount;

// 擴容的閾值, capacity * load factor
int threshold;

// 裝填因子
float loadFactor;
複製代碼

put元素

若是你看懂了這個過程,那麼基本上 HashMap 的主要邏輯就算是基本理解了

步驟

  1. 判斷 key 是否爲空,若是爲空直接放到 table[0]的位置,若是不爲空,通過運算肯定其在table中的下標
  2. 而後再判斷相應的索引上是否已經有元素了,沒有的話,直接修改;有的話再判斷key值是否相等,相等的話,直接覆蓋value,不相等的話遍歷鏈表(紅黑樹),並插入到鏈表最後
  3. 在第二步的插入時,先判斷 ++size是否已經大於了閾值,大於須要擴容。

稍微詳細些的步驟看下方思惟導圖,一樣縮進的爲 if-else 關係

還有的細節沒有寫,待會兒跟着源碼再細講,我就跟着源碼的調用順序分析

那麼假如我如今執行下面的語句,他到底怎麼執行

import java.util.HashMap;

public class Test {
    public static void main(String[] args) {
        HashMap hashMap = new HashMap();
        hashMap.put("name","zhangSan");
        
    }
}
複製代碼

第一條語句-構造函數

public MyHashMap(int initCapacity,float loadFactor) {
        if(initCapacity<0)
            throw new IllegalArgumentException("初始化容量失敗: "+
                                                    initCapacity);
        if(initCapacity>= MAX_CAPACITY)
            initCapacity= MAX_CAPACITY;
        if(loadFactor<=0||Float.isNaN(loadFactor))
            throw new IllegalArgumentException("裝填因子不合法"+
                                                    loadFactor);
        this.loadFactor=loadFactor;
        this.threshold=tableSizeFor(initCapacity);

    }
public MyHashMap(int initCapacity) {
    this(initCapacity,DEFAULT_LOADFACTOR);
}

/** * 無參的,所有默認 */
public MyHashMap() {
    this.loadFactor=DEFAULT_LOADFACTOR;
}


public MyHashMap(Map m){
    this.loadFactor=DEFAULT_LOADFACTOR;

}
複製代碼

若是沒有傳入參數,他就會調用無參的構造器,那麼默認的長度爲 16,DEFAULT_INITIAL_CAPACITY,默認的裝填因子爲 0.75,DEFAULT_LOAD_FACTOR,傳入範圍(0,1];

注意:這個時候,數組尚未初始化,僅僅是定義了一個Entry類型的數組

第二條語句

執行hashMap.put("name","zhangSan")

put函數

首先他在源碼中是這樣的,他又調用了putVal函數,專門存入元素的函數(ps:源碼 611行)

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
複製代碼

他傳入了5個值,可是咱們先重點關注前三個值,第一個是要存入的key的hash值,第二個是key,第三個是value,至於K,V泛型若是不瞭解,你能夠理解爲 Object類型,若是按照測試的語句,你就能夠把它當成 String類型。

這個put函數,他有返回值,返回值是null,或者oldValue,看了下面的putValue函數你就知道了

hash 函數,計算哈希值

傳入這個參數是爲了建立節點node以及計算索引時用

源碼(第337 行)

static final int hash(Object key) {
    int h;
  	// 將key 的高16位和低16位進行異或
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
複製代碼

這個也是 JDK 1.8的改進,1.7不是這樣的。

改進的目的

主要是從速度、功效、質量來考慮的,這麼作能夠在數組table的n比較小的時候,也能保證考慮到高低bit都參與到hash的計算中(爲了是分佈更均勻),同時不會有太大的開銷。

putValue函數

private Object putVal(int hash, Object key, Object value, boolean onlyIfAbsent, boolean evict) {
        Entry[] tab;
        Entry p;
    int n,i;
    // 若是第一次 進行存放數據,進行初始化,table 被延遲到進行數據存放時才初始化
    if((tab = table) == null || (n = table.length)==0){
        n = (tab = resize()).length;
    }
    if((p = table[i = ((n - 1) & hash)]) == null){
        tab[i] = newEntry(hash,key,value,null);
    }

    else {
        Entry e;
        Object k;
        // 若是 key 相同,那麼就直接將 value 覆蓋
        // 爲何要比較這麼屢次

        // 1.首先判斷 哈希值是否相同
        if(p.h == hash &&
                // 2.判斷兩個key是否相等,使用 '==' 是非字符串狀況,之比較兩個的內容,使用'equals' 是針對字符串
                (((k = p.key) == key) || (key != null && key.equals(k))))
            // 覆蓋value值
            e = p;

        // 這個是樹的狀況
        //else if(p instance of TreeNode)

        // 鏈
        else{
            for(int binCount=0;;++binCount){
                // 遍歷到最後,插入
                if((e = p.next) == null){
                    p.next = newEntry(hash,key,value,null);

                    /* 若是 binCount >=轉化樹的閾值-1 ,則將鏈表轉化爲樹 if(binCount >= TREEIFY_THRESHOLD-1) treeifyBin(tab,hash); */
                    break;
                }
                if(p.h == hash &&
                        (((k = p.key) == key) || (key != null && key.equals(k))))
                    break;
                // 移動到下一個
                p = e;
            }



            // 若是有相應的映射,即key相同
            if(e != null){
                 Object oldValue = e.value;
                 if(!onlyIfAbsent || oldValue == null)
                     e.value = value;
                 return oldValue;
            }

        }

    }
    // 修改次數 ++
    ++ modCount;

    // 大於閾值就擴容
    if(++size >threshold)
        resize();
  
    //afterNodeInsertion(evict);

    return null;

}
複製代碼
返回值

看了上面的源碼分析你就能解決上面的疑問,put函數有返回值,返回值爲null或者oldValue

先記住答案:當他不產生覆蓋的時候,返回null;當他產生覆蓋的時候返回 oldVal,即原來被覆蓋的值

咱們先進行測試,你就大概知道意思了

import java.util.HashMap;

public class Test {
    public static void main(String[] args) {
        HashMap hashMap = new HashMap();
        hashMap.put("name","張三");

        Object oldValue1 = hashMap.put("name","李四");
        Object oldValue2 = hashMap.put("age",18);
        System.out.println("oldValue = " + oldValue1);
        System.out.println("oldValue2 = " + oldValue2);
    }
}
複製代碼

我想如今你應該清楚了,當輸入的key的內容相同,hash值也相同的時候,他就會覆蓋以前的Value值,而且返回被覆蓋前的value值。(假設輸入的只是String類型,若是是自定義的對象,須要重寫 hashCode 和 equals 方法)

這個的關鍵代碼在上面函數的++modCount一行上面,我有註釋

//若是有相應的映射,即key相同
if(e != null){
    Object oldValue = e.value;
    if(!onlyIfAbsent || oldValue == null)
      e.value = value;
    return oldValue;
}
複製代碼
分析putValue函數
條件

首先要判斷table數組是否初始化了,即這條語句if ((tab = table) == null || (n = tab.length) == 0)

  • 若是沒有初始化則要調用resize方法(後面分析).能夠直接看索引爲 resize函數的內容
  • 若是已經初始化了,就須要計算元素的索引了(這個是很是重要的一步,也是他爲啥能在O(1)的時間複雜度內找到在數組中的相應位置)
計算索引

將 key 的 hash 值和table.length-1相與,相與的結果就是要存入的元素的table中的 位置tab[(n - 1) & hash]

這個時候看源碼,它分爲兩種狀況:

第一種:相應的索引上沒有元素(只有這個時候 size才++,相應索引上有元素,size是不會 ++ 的)

// 若是table 數組的相應的索引上沒有元素,那麼直接建立一個新的節點
if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);
// 修改次數++
++modCount;
// 判斷是否須要擴容
if (++size > threshold)
  resize();
afterNodeInsertion(evict);
return null;
複製代碼

如今知道啥時候返回 null了吧

第二種:相應的索引上有元素

這個時候就要判斷元素的key是否相等if(p.h == hash &&(((k = p.key) == key) || (key != null && key.equals(k))))

else {
    Entry e;
    Object k;
    // 若是 key 相同,那麼就直接將 value 覆蓋
    // 爲何要比較這麼屢次

    // 1.首先判斷 哈希值是否相同
    if(p.h == hash &&
            // 2.判斷兩個key是否相等,使用 '==' 是非字符串狀況,之比較兩個的內容,使用'equals' 是針對字符串
            (((k = p.key) == key) || (key != null && key.equals(k))))
        // 覆蓋value值
        e = p;

    // 這個是樹的狀況
    //else if(p instance of TreeNode)

    // 鏈
    else{
        for(int binCount=0;;++binCount){
            // 遍歷到最後,插入
            if((e = p.next) == null){
                p.next = newEntry(hash,key,value,null);

                /* 若是 binCount > 轉化樹的閾值 ,則將鏈表轉化爲樹 if(binCount >= TREEIFY_THRESHOLD-1) treeifyBin(tab,hash); */
                break;
            }
            if(p.h == hash &&
                    (((k = p.key) == key) || (key != null && key.equals(k))))
                break;
            // 移動到下一個
            p = e;
        }


        // 若是有相應的映射,即
        if(e != null){
             Object oldValue = e.value;
             if(!onlyIfAbsent || oldValue == null)
                 e.value = value;
             return oldValue;
        }
    }
}
複製代碼

這就是返回 oldValue的狀況,固然上面的也有狀況並不會返回oldValue

resize函數

這個是進行擴容的函數,也是很是重要的,要確保每次擴容先後容量大小都是2的n次方。而且在JDK 1.8中,對這個函數進行了優化,使得算法很是的高效

調用的情景
  1. 初始化 數組table。在putVal函數中,(源碼第628行)

    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    複製代碼
  2. 進行擴容數組table的size達到閾值時,即++size > load factor * capacity 時,也是在putVal函數中

    if (++size > threshold)
        resize();
    複製代碼
執行邏輯

源碼註釋

忽略了樹的邏輯,只有相應的條件

final  Entry[] resize() {
    // 定義舊的數組爲 Entry 類型的數組,oldTab
    Entry[] oldTab = table;
    // 若是oldTab==null 則返回 0,不然返回數組大小
    int oldCap = (oldTab==null) ? 0 : oldTab.length;

    int oldThreshold = threshold;

    int newCap=0,newThreshold=0;

    // 說明已經不是第一次 擴容,那麼已經初始化過,容量必定是 2的n次方,因此能夠直接位運算
    if(oldCap>0){
        // 若是 原來的數組大小已經大於等於了最大值,那麼閾值設置爲 Integer的最大值,即不會再進行擴容
        if(oldCap >= MAX_CAPACITY){
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }

        // 所以已經不是第一次擴容,必定是2的n次方
        else if ((newCap = oldCap << 1) < MAX_CAPACITY &&
                  oldCap >= INIT_CAPACITY)

            newThreshold = oldThreshold << 1;

    }
    // 若是oldThreshold > 0,而且oldCap == 0,說明是尚未進行調用resize方法。
    // 說明輸入了初始值,且oldThreshold爲 比輸入值大的最小的2的n次方
    // 那麼就把 oldThreshold 的值賦給 newCap ,由於這個值如今爲 比輸入值大的最小的2的n次方
    else if(oldThreshold>0)
        newCap = oldThreshold;

    // 知足這個條件只有調用無參構造函數,注意只有;
    else{
        newCap = INIT_CAPACITY;
        newThreshold = (int) (INIT_CAPACITY * DEFAULT_LOADFACTOR);
    }

    if(newThreshold == 0){

        float ft = (float) (newCap * loadFactor);
        newThreshold =(newCap < MAX_CAPACITY && ft < (float) MAX_CAPACITY ?
                (int )ft : Integer.MAX_VALUE);
    }

    threshold = newThreshold;

    Entry newTable[] = new Entry[newCap];
    table=newTable;

    // 將原來數組中的全部元素都 copy進新的數組
    if(oldTab != null){
        for (int j = 0; j < oldCap; j++) {
            Entry e;

            if((e = oldTab[j]) != null){
                oldTab[j] = null;

                // 說明尚未成鏈,數組上只有一個
                if(e.next == null){
                    // 從新計算 數組索引 值
                    newTable[e.h & (newCap-1)] = e;

                }
                // 判斷是否爲樹結構
                //else if (e instanceof TreeNode)


                // 若是不是樹,只是鏈表,即長度尚未大於 8 進化成樹
                else{
                    // 擴容後,若是元素的 index 仍是原來的。就使用這個lo前綴的
                    Entry loHead=null, loTail =null;

                    // 擴容後 元素index改變,那麼就使用 hi前綴開頭的
                    Entry hiHead = null, hiTail = null;
                    Entry next;
                    do {
                        next = e.next;
                        if((e.h & oldCap) == 0){
                            // 若是 loTail == null ,說明這個 位置上是第一次添加,沒有哈希衝突
                            if(loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else{
                            if(hiTail == null)
                                loHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e ;
                        }

                    }while ((e = next) != null);


                    if(loTail != null){
                        loTail.next = null;
                        newTable[j] = loHead;
                    }

                    // 新的index 等於原來的 index+oldCap
                    else {

                        hiTail.next = null;
                        newTable[j+oldCap] = hiHead;
                    }

                }
            }

        }
    }

    return newTable;
}
複製代碼
重要:擴容後元素的位置
// 將原來數組中的全部元素都 copy進新的數組
if(oldTab != null){
    for (int j = 0; j < oldCap; j++) {
        Entry e;

        if((e = oldTab[j]) != null){
            oldTab[j] = null;

            // 說明尚未成鏈,數組上只有一個
            if(e.next == null){
                // 從新計算 數組索引 值
                newTable[e.h & (newCap-1)] = e;

            }
            // 判斷是否爲樹結構
            //else if (e instanceof TreeNode)


            // 若是不是樹,只是鏈表,即長度尚未大於 8 進化成樹
            else{
                // 擴容後,若是元素的 index 仍是原來的。就使用這個lo前綴的
                Entry loHead=null, loTail =null;

                // 擴容後 元素index改變,那麼就使用 hi前綴開頭的
                Entry hiHead = null, hiTail = null;
                Entry next;
                do {
                    next = e.next;
                  	//這個很是重要,也比較難懂,將它和原來的長度進行相與,就是判斷他的原來的hash的上一個 bit 位是否爲 1.下面我再詳細說
                    if((e.h & oldCap) == 0){
                        // 若是 loTail == null ,說明這個 位置上是第一次添加,沒有哈希衝突
                        if(loTail == null)
                            loHead = e;
                        else
                            loTail.next = e;
                        loTail = e;
                    }
                    else{
                        if(hiTail == null)
                            loHead = e;
                        else
                            hiTail.next = e;
                        hiTail = e ;
                    }

                }while ((e = next) != null);


                if(loTail != null){
                    loTail.next = null;
                    newTable[j] = loHead;
                }

                // 新的index 等於原來的 index+oldCap
                else {

                    hiTail.next = null;
                    newTable[j+oldCap] = hiHead;
                }

            }
        }

    }
}
複製代碼

從上面的代碼能夠看出來,他遍歷數組。將每一個元素和原來的數組長度進行與運算,判斷是否爲 0

若是爲0,那麼索引位置不變,

若是不爲 0,那麼索引位置等於 原來的索引+原來的數組長度,

你可能有點納悶,爲啥要這樣,請參考下這篇文章。

不過閱讀前,我以爲得了解這些前提,

  1. **數組table的長度絕對是2的n次方(必定是)。**至於爲啥你能夠參考另外一篇文章"table長度究竟是多少"

知道這個前提,那麼你就知道在數組的長度中,只有最高位是1,其餘全爲0;

  1. 元素在數組table的索引位置是 (key.hash&(table.length-1))

文章連接:www.jianshu.com/p/4177dc15d…

上面的這個算法很是重要,也是JDK1.8以後的優化,效率很是高

最後

至此,put一個元素的過程基本就完了,可能還有一些小細節沒講到(應該不過重要,能夠自行查看個人註釋)

若是你put方法搞懂了,那麼後面的get,contains,remove,iterator 這些基本沒有啥大的障礙,這些搞懂,hashMap的 70% 至少都懂了

後面應該還有上述方法的源碼分析以及回答一些疑問。

好比"爲啥hashMap的數組長度必定是2的n次方",

​ "當我new HashMap()的時候,輸入的初始容量 0,1,2,3,4,5,6。table初始化的值到底爲多少"

等等

相關文章
相關標籤/搜索