HashMap源碼理解(一)

HashMap設計的初心是找到一種方法,可以存儲一組鍵值對的集合,並能夠快速的實現查找元素。

Map的定義:是將鍵映射到值的對象

HashMap中是利用內部類Node來定義存儲這個鍵值對的,之後利用Node數組來存儲HashMap數據結構,那我們都知道數組之所以能夠快速的查找,是因爲其具有索引(數組下標)直接定位到對應的存儲桶(數組所存儲對象的位置)。而在HashMap中爲了能夠利用索引來查找相應的Key,我們需要建立一種Key->index的映射關係。這樣每次我們要查找一個key時,首先根據映射關係,計算出對應的數組下標,然後根據對應的數組下標,直接找到對應的key-value對象,這樣基本能以O(1)的時間複雜度得到結果。

而在這裏,將key映射成index的方法稱爲hash算法,我們希望它能將key均勻分佈在數組中。

而使用Hash算法同時也補足了數組插入和刪除性能差的短板,我們都知道,數組之所以性能差是因爲他是順序存儲的,在一個位置插入節點或者刪除節點需要一個個移動他的後續節點騰出位置或者覆蓋位置。

而使用hash算法後,數組不在按照順序存儲,插入刪除操作只需要關注一個存儲桶即可,而不再需要額外的操作。

而在我們實際操作中,我們所使用的的Hash算法雖然能夠將key均勻分佈在數組中,但是它只能夠儘量的做到,並不是絕對的,更何況我們的數組大小有限,中間可定會應爲Hash算法就兩個不同的Key映射成同一個index值,這就產生了Hash衝突,也就是兩個Node要存儲在數組中的同一個位置中該怎麼辦

在在解決這個Hash衝突的過程中,我們經常採用的方法是通過選擇鏈地址法來解決,即在產生衝突的存儲桶中改爲單鏈表存儲。

所以採用了HashMap中的Node類的基本架構

class Node<K,V> implements Map.Entry<K,V> {

    final int hash;

    final K key;

    V value;

    Node<K,V> next;

   

    Node(int hash, K key, V value, Node<K,V> next) {

        this.hash = hash;

        this.key = key;

        this.value = value;

        this.next = next;

    }

    ...

}

我們都知道, 鏈表查找時,只能通過順序查找來實現,因此時間複雜度爲O(n),

這就會導致如果key值被Hash算法映射到一個存儲桶上,將會導致存儲桶上的鏈表長度越來越長,此時,數組的查找會逐漸退化爲鏈表查找,所以時間複雜度就變爲了O(n)。

爲了解決這個問題,在Java1.8之後,單鏈表長度超過8之後,將會自動的將鏈表轉化爲紅黑樹,達到將複雜度降爲O(logN)的時間複雜度。從而提高查找性能。

在前面的時候,我們都知道,解決Hash衝突有兩個方法,利用鏈表的形式來解決,但是,還有一個客觀的因素就是數組本身的長度,前者不管雖然一定程度上的可以解決Hash衝突,但他所帶來的的效益並不是很高,當到達一定的程度之後,不得不採取擴大HashMap本身數組的大小就成爲了首選。那到底在實際過程中我們應該如何去擴容,以及每次擴容多少較爲合適那。

所以,到現在,我們就必須對擴容做一個瞭解。

數組擴容是一個很耗費CPU資源的動作,需要將原數組的內容複製到新的數組中,因此,如果擴容動作過於頻繁,其必然會導致性能降低。

如何做到合適的擴容,JDK源碼resize函數中有實現

未完待續。。。。。。。。。。。。。。。。。。。。。。。。。。。。。

 

我們知道HashMap中的Key是泛型,所以,我們在實際應用過程中,我們需要將Object先轉化爲int,之後,再轉化爲index。所以,我們會首先通過Hash算法將key映射成整數,然後將整數映射成爲有限的數組下標。通常在將整數映射成爲有限的數組下標過程中,我們都是採用對數組長度的取模運算。即是

Key.hashCode()%table.length

那麼HashMap是這樣做的的嗎?以下是HashMap的散列算法

static final int hash(Object key) {

    int h;

    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

}

Int類型是32位,h^h>>>16,其實就是講Hashcode的高16位和低16位進行異或運算。這樣做是爲了充分利用高半位和低半位的信息,對低位進行了擾動,目的就是爲了該HashCode 映射成數組的下標時更加均勻。詳細的解釋可以參考https://www.zhihu.com/question/20733617/answer/111577937

從這個HashMap中key值可以爲NULL,且NULL值一定存儲爲數組的第一個位置(Why)

我們上面在對int轉化成index的過程中,我們通常會想到的是MOD方法來實現,但是,由於HashMap的數組長度2^n,此時,利用下面公式可以做到性能的提升。

任意整數對2^n取模等效於:

h % 2^n = h & (2^n -1)

這樣我們就將取模操作轉換成了位操作, 而位操作的速度遠遠快於取模操作.

爲此, HashMap中, table的大小都是2的n次方, 即使你在構造函數中指定了table的大小, HashMap也會將該值擴大爲距離它最近的2的整數次冪的值.

那我們就來看一下HashMap函數是如何每次構造出距離它最近的2的整數次冪的值。

觀看源代碼其中最爲重要的部分是在

this.threshold = tableSizeFor(initialCapacity);

/**

 * Returns a power of two size for the given target capacity.

 */

static final int tableSizeFor(int cap) {

    int n = cap - 1;

    n |= n >>> 1;

    n |= n >>> 2;

    n |= n >>> 4;

    n |= n >>> 8;

    n |= n >>> 16;

    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;

}

tableSizeFor方法用於找到大於等於initialCapacity的最小的2的冪

我們接下來就來看一下這個代碼的是如何來找到的,

當一個32位的整數不爲0的時候,32bit中至少有一個位置爲1,上面的2個移位操作目的在於從最高位的1開始,一直到最低位的所有bit全部設爲1,最後再加1(注意,一開始的時候,是先cap-1的)則得到的數就是大於等於initialCapa的最小的2次冪。詳細的請參考這篇博客https://blog.csdn.net/fan2012huan/article/details/51097331

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