java開發:集合(四):hashMap源碼解析

HashMap底層存儲結構

HashMap是一個用於存儲Key-Value鍵值對的集合,每一個鍵值對其實就是HashMap內部的Entry類對象。

 static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;
        /**
         * Creates new entry.
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }

Entry是HashMap的內部類,用來保存我們的鍵值,next指向下一個節點,hash用來保存key值的哈希碼

HashMap它底層是基於數組鏈表實現的數據存儲結構。
HashMap在初始化時會創建一個默認長度爲16的數組,當我們添加元素時它會根據key值的哈希碼和數組長度取餘得到元素在數組的存儲位置。但是存在的問題就是不同的key值在經過計算之後可能會映射到相同的位置上,當插入一個元素時,發現該位置已經被佔用,這時候就會產生衝突,也就是所謂的哈希衝突,所以HashMap結合鏈表正是解決了位置衝突問題。
HashMap 設置數組的每一個元素對應一個鏈表的頭結點。當位置發生衝突時就往該鏈表的頭部插入新的節點,新的節點指向舊的頭結點。

在這裏插入圖片描述
HashMap存儲數據的流程:

如上圖: 當添加一個新的元素時先計算出元素在數組的存儲下標,如果位置是空的直接插入到數組,如果位置不爲空判斷key值是否相等,相等則覆蓋value值,不相等則歷遍鏈表。歷遍鏈表結束後key還是沒找到則往鏈表的頭部插入新的節點。
像上圖數組第一個位置存放着一個Entry對象,當插入新Entry對象計算出的位置也是數組的第一個位置,這時候發生哈希衝突了。系統會把新的Entry插入到數組的第一個位置,並且新的Entry.next屬性指向舊的Entry對象。

HashMap數據查找流程:

因爲HashMap在內部維護這一個數組table,數組的每個位置保存着每個鏈表的表頭結點,查找元素時,先通過hash函數得到key值對應的hash值,再根據hash值和數組長度計算得到在數組中的索引位置,拿到對應的鏈表的表頭,最後去遍歷這個鏈表,得到對應的value值。

put()方法源碼解析:

 public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        //計算key的哈希值
        int hash = hash(key);
        //根據key哈希值和數組長度計算出存儲下標
        int i = indexFor(hash, table.length);
        //歷遍table[i]整個鏈表,如果出現key重複的則覆蓋value值,然後return結束程序
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        //上面的for循環結束後沒有發現key沒有重複則會執行這個方法
        addEntry(hash, key, value, i);
        return null;
    }


 private V putForNullKey(V value) {
 		//獲取數組的第一個位置元素,歷遍鏈表找到key爲null的鍵值對然後覆蓋value值
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                //結束方法
                return oldValue;
            }
        }
        modCount++;
        //上面的for循環結束後沒有發現key爲null的元素則會執行這個方法
        addEntry(0, null, value, 0);
        return null;
    }

	
  void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }
		//創建節點
        createEntry(hash, key, value, bucketIndex);
    }
    
 void createEntry(int hash, K key, V value, int bucketIndex) {
 		//根據bucketIndex獲取數組指定位置的元素,e 是鏈表的頭結點
        Entry<K,V> e = table[bucketIndex];
        //創建節點放到數組中,這時候該節點成爲頭結點,同時它的next指向上一個頭結點e
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }

先是判斷key是否爲null,是則執行putForNullKey(),putForNullKey歷遍table[0]整個鏈表,如果有key等於null的元素時則覆蓋value值,然後結束程序(因此haspMap只能有一個key爲null的元素)。如果table[0]整個鏈表沒有key等於null的元素則執行 addEntry(0, null, value, 0),addEntry調用createEntry(),createEntry就是將table[0]的元素取出來,然後把新的元素放到table[0]中,同時新的元素指向舊的元素,鏈表size++
當key不爲null時,先根據key的哈希值和數組長度計算出存儲的下標位置,歷遍table[i]的整個鏈表看有沒有key重複,如果出現key重複的則覆蓋value值,然後return結束程序。否則執行addEntry(),addEntry調用createEntry(),createEntry就是將table[i]的元素取出來,然後把新的元素放到table[i]中,同時新的元素指向舊的元素,鏈表size++

get()方法源碼解析:

 public V get(Object key) {
 		//先判斷key是否爲null,是則執行getForNullKey
        if (key == null)
            return getForNullKey();
            //key不等於null,執行getEntry
        Entry<K,V> entry = getEntry(key);
        return null == entry ? null : entry.getValue();
    }
    
    
  private V getForNullKey() {
  		//因爲key=null的元素hashMap都是存放在table[0]指向的鏈表中
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        	//歷遍找到key=null的元素
            if (e.key == null)
                return e.value;
        }
        return null;
    }
    
 final Entry<K,V> getEntry(Object key) {
 		//計算key的哈希值
        int hash = (key == null) ? 0 : hash(key);
        //indexFor(hash, table.length)是根據key哈希值和table長度計算元素在數組的索引,然後for循環歷遍整個table[i]
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            //比較hash值和key值,找到元素後返回
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }

先是判斷key是否爲null,是則調用getForNullKey(),getForNullKey內部則會歷遍table[0]指向的鏈表。
若key不等於null,則調用getEntry(),getEntry根據計算算出數組下標i,然後歷遍table[i],找不到元素返回null。

最後聲明一點,這是基於jdk1.7的源碼分析。1.8後對hashMap進行了優化。
1.7採用數組+單鏈表,1.8在單鏈表超過一定長度後改成紅黑樹存儲
1.7擴容時需要重新計算哈希值和索引位置,1.8並不重新計算哈希值,巧妙地採用和擴容後容量進行&操作來計算新的索引位置。
1.7插入元素到單鏈表中採用頭插入法,1.8採用的是尾插入法。

因此引申倆個問題:
1.爲啥1.7之前元素添加是採用頭插入法

因爲hashMap設計者們認爲新加的數據被訪問的機率大於舊的數據,所以放在前面訪問更快。

2.爲啥1.8之後元素添加改爲了尾插入法

HashMap在jdk1.7之前採用頭插入法,在擴容時會導致鏈表的順序倒置,在線程併發的情況下擴容容易導致鏈表死循環(即倆個節點的next節點相互指向對方),並且新加的數據被訪問的機率大於舊的數據這個說法並不成立,而尾插法在擴容的時候節點順序不會打亂。
jdk1.8之後HashMap爲何從頭插入改爲尾插入

3.1.8之後對計算元素索引進行了優化。未擴容前HashMap通過哈希值的二進制和數組長度-1的二進制進行按位與運算得到的結果就是下數組的索引,(圖是網上覆制的)

&是二進制“與”運算,參加運算的兩個數的二進制按位進行運算,運算的規律是:
0 & 0=0
0 & 1=0
1 & 0=0
1 & 1=1

例如:一個key的哈希值二進制是 0001 1010 ,數組長度是n=16,二進制:10000,n-1二進制是1111
哈希值和n-1進行與運算得到二進制:1010 轉成十進制就是10,即索引就是在table[10]

在這裏插入圖片描述
當數組擴容後n=32 ,二進制是:100000,n-1的二進制是:11111(n-1的最高位和舊數組的最高位相同),當然我們依舊通過上訴的計算也是可以得到每個元素的索引,但是沒必要。你會發現當n-1的最高位對應的哈希值二進制數是0的話計算出來的索引不變,對應的是1則計算出來的結果是原位置+舊數組長度。
例如下圖:擴容後n-1的最高位是1(往左數第五個數),最高位對應hash1的二進制數是1,因此計算出來的結果是26,最高位對應hash2的二進制數是0,因此索引保持不變。又因爲舊數組的最高位和n-1的最高位是一樣的,因此擴容的時候HashMap通過(e.hash & oldCap) == 0判斷節點是否爲新位置節點,等於1則移動到原位置+舊數組長度的索引(數組長度永遠是2的次冪,二進制只有最高位是1其他是0,因此不管誰和它進行&算要麼得1要麼得0)
在這裏插入圖片描述

現在推薦使用 ConcurrentHashMap,它是Java中的一個線程安全且高效的HashMap實現。

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