HashMap源碼分析及衝突處理的細節

一.  首先看一下hashmap的數據結構,可以看到是數組加鏈表實現的。

transient Entry<K,V>[] table =(Entry<K,V>[]) EMPTY_TABLE;

可以看到它的實現是一個Entry<K,V>類型的名爲table的數組。而Entry是HashMap中的一個內部類。

static class Entry<K,V> implementsMap.Entry<K,V> {

       final K key;

       V value;

       Entry<K,V> next;

       int hash;

       它有四個屬性,key,value,next,hash。由於有next屬性,所以自然會想到鏈表的結點類,事實上,當出現hash衝突時,由於HashMap使用鏈地址法來解決衝突。所以table數組的每一個元素就會形成鏈表結構。所以可以說HashMap就是一個存儲鏈表的數組。

  

二.   HashMap的table數組的默認大小是16,並且大小永遠是2的n次方。它還有一個負載因子,默認爲0.75,可以通過帶參數的構造方法自己指定。負載因子loadFactor的作用是:HashMap中的實際的數據大小除以總容量(initialCapacity),當值達到loadFactor時,HashMap的總容量自動擴展一倍。

  staticfinal int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

static final float DEFAULT_LOAD_FACTOR = 0.75f;

 

   計算threshold,值爲capacity *loadFactor。

threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY +1);

 

   這裏就會判斷,當size的值大於threshold(即capacity *loadFactor)時,就會進行擴容。

if ((size >= threshold) && (null != table[bucketIndex])){

            resize(2 * table.length);

 

 

三.接下來以put方法作爲入口,進行分析。

1.首先進行hash運算,並求出將要存入的數組下標。

int hash = hash(key);

int i = indexFor(hash, table.length);

 

     接下來看看計算下標的算法是如何實現的。進入到indexFor方法中,實現的代碼如下:

static int indexFor(int h, int length) {

        // assertInteger.bitCount(length) == 1 : "length must be a non-zero power of2";

        return h &(length-1);

    }

 

    具體是h &(length-1),這樣計算的值介於0和length-1之間,有點類似於hash%length 的求模運算。之所以用&運算我認爲是位運算的效率更高吧。

 

2.然後是下面這段代碼:

 

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);

                returnoldValue;

            }

        }

 

        modCount++;

        addEntry(hash, key,value, i);

 

 

        會判斷table[i]是否爲null,這是會出現兩種情況,先分析第一種情況,即table[i]還沒有元素,是null的情況,這時循環就沒有執行,繼續往下,去執行addEntry方法。addEntry方法中先進行判斷是否需要擴容,如果需要,就進行擴容。然後又進入到createEntry方法中。它的代碼實現如下:

 

void createEntry(int hash, K key, V value, int bucketIndex) {

        Entry<K,V> e =table[bucketIndex];

        table[bucketIndex] =new Entry<>(hash, key, value, e);

        size++;

    }

 

        它做的工作就是把hash,key, value, e四個屬性組裝成一個Entry的對象e,並將它放在數組下標相應的位置,這時如果加入的是第一個元素,e則爲null,所以next指向了null。最後再把size加1.

 

        下面分析第二種情況,即即table[i]已經有了元素,不是null的情況。這時會執行上面的那一段for循環,這個循環的作用就是依次遍歷整個table[i]鏈表,並且判斷這個鏈表的每一個元素的key是否和新加進來的元素的key相同,如果相同新的value就會覆蓋舊的value,即保證HashMap中唯一的key有唯一的value.

         進行完了覆蓋的操作後,就會執行剩下的代碼,和第一種情況一樣,執行addEntry方法。addEntry方法中先進行判斷是否需要擴容,如果需要,就進行擴容。再執行createEntry方法。這時e = table[bucketIndex];計算出來的e就不爲null了,爲原來的i下標處的元素。然後又封裝一個新的Entry對象,放入到table[i]位置,它的next指向了e,即原來的table[i]處的元素。

        所以通過分析我們可以發現,最後放入的元素總是在這個衝突鏈表的表頭的位置。

        最後,可以看到,當出現衝突時,會把數據放入鏈表中,每次插入新的元素都會對整個鏈表進行遍歷操作,影響程序的效率。所以當我們向HasnMap中放入的key的數據類型是自定義類型的時候,要按照規範合理的實現hashcode和equals方法,儘量避免衝突。另外,由於它的底層實現也是數組,所以也要儘量避免擴容。最好能估算出初始的大小,而對於負載因子,據說0.75是計算出的最佳值,所以還是用默認的吧。

 

 

 

發佈了41 篇原創文章 · 獲贊 7 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章