JDK1.7 HashMap 源碼分析

概述

HashMap是Java裏基本的存儲Key、Value的一個數據類型,瞭解它的內部實現,能夠幫咱們編寫出更高效的Java代碼。html

本文主要分析JDK1.7中HashMap實現,JDK1.8中的HashMap已經和這個不同了,後面會再總結。java

正文

HashMap概述

HashMap根據鍵的hashCode值獲取存儲位置,大多數狀況下能夠直接定位到它的值,於是具備很快的訪問速度,但遍歷順序倒是不肯定的。 HashMap最多隻容許一條記錄的鍵爲null,容許多條記錄的值爲null。HashMap非線程安全,即任一時刻能夠有多個線程同時寫HashMap,可能會致使數據的不一致。若是須要知足線程安全,能夠用 Collections的synchronizedMap方法使HashMap具備線程安全的能力,或者使用ConcurrentHashMap。算法

HashMap的存儲結構以下圖所示:數組

_thumb6

HashMap根據鍵的hashCode值和HashMap裏數組的大小取餘,餘數即爲該Key存儲的數組位置。安全

如:一個Key的hashCode爲15,HashMap的Size爲6,15 % 6 = 3,因此該Key存儲在數組的第三個位置。數據結構

考慮另外一種狀況,若是一個Key的hashCode爲21,那21 % 6 = 3,因此該Key也存儲在數組的第三個位置,這樣豈不是重複了?app

因此對於在同一個位置的Key,HashMap把他們存儲在一個單向鏈表裏,新的Key永遠在最前面。函數

若是這個數組裏存儲的太滿,HashMap還有擴容機制。性能

下面咱們分析HashMap的源代碼,來看看數據是怎麼存儲的。ui

PUT

    public V put(K key, V value) {
        //判斷若是table爲空,則初始化table
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        if (key == null)
            return putForNullKey(value);
        //計算key的hash值
        int hash = hash(key);
        //根據key的hash值和table.length計算KEY的位置
        int i = indexFor(hash, table.length);
        //判斷是否有重複的值,如有,則用新值替換舊值,並返回舊值
        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;
            }
        }

        //修改的次數加一,用於迭代HashMap時,判斷HashMap元素有沒有修改
        modCount++;
        //添加key
        addEntry(hash, key, value, i);
        return null;
    }

inflateTable — 初始化HashMap內部數組

private void inflateTable(int toSize) {
    //根據toSize計算容量,即大於toSize的最小的2的n次方
    int capacity = roundUpToPowerOf2(toSize);
    ………
}

private static int roundUpToPowerOf2(int number) {
    // assert number >= 0 : "number must be non-negative";
    return number >= MAXIMUM_CAPACITY
            ? MAXIMUM_CAPACITY
            : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}

public static int highestOneBit(int i) {
    // HD, Figure 3-1
    i |= (i >>  1);
    i |= (i >>  2);
    i |= (i >>  4);
    i |= (i >>  8);
    i |= (i >> 16);
    return i - (i >>> 1);
}

關鍵方法Integer.highestOneBit((number - 1) << 1),這個方法的結果就是求出大於給定數值的,最小的2的N次方。

解釋以前先說明幾個概念:

<< : 按二進制形式把全部的數字向左移動對應的位數,高位移出(捨棄),低位的空位補零。在數字沒有溢出的前提下,對於正數和負數,左移一位都至關於乘以2的1次方,左移n位就至關於乘以2的n次方;

>>: 按二進制形式把全部的數字向右移動對應位移位數,低位移出(捨棄),高位的空位補符號位,即正數補零,負數補1。右移一位至關於除2,右移n位至關於除以2的n次方。

>>>: 無符號右移,忽略符號位,空位都以0補齊

 

咱們拿數字10作示例,通過(number - 1) << 1 = 18,二進制表示爲:10010

i |= (i >>  1) 即:10010 | 01001 = 11011

i |= (i >>  2) 即:11011 | 00110 = 11111

i |= (i >>  4) 即:11111 | 00001 = 11111

……

其實這幾步就是把i的最高位1以後的全部位都變成1

而後 i – (i >>> 1) 即:11111-01111=10000(16)

這步是把最高位,以後的都變成0,這樣就求出了最接近10的2的N次方(16)

至於爲何要把數組的Size設置爲2的N次方,咱們後面說。

hash — 計算Key的hash值

final int hash(Object k) {
    int h = hashSeed;
    if (0 != h && k instanceof String) {
        return sun.misc.Hashing.stringHash32((String) k);
    }

    h ^= k.hashCode();

    // This function ensures that hashCodes that differ only by
    // constant multiples at each bit position have a bounded
    // number of collisions (approximately 8 at default load factor).
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

根據上面的註釋,咱們能夠看出,HashMap中使用的hash值,不是Key直接的hashCode,而是通過一系列計算的。

計算hash值的做用就是,讓hash的高位也參與indexFor運算,避免hash碰撞,儘可能減小單向鏈表的產生,由於鏈表中查找一個元素時間複雜度爲O(n)。

具體怎麼避免的,咱們放到下面這個indexFor裏說。

indexFor — 計算Key所對應的數組位置

static int indexFor(int h, int length) {
    // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
    return h & (length-1);
}

第一次看到這個方法非常不理解,不是應該用 h % length嗎?其實這裏用了一個很是巧妙的方法來取這個餘數。

在計算機中CPU作除法運算、取餘運算耗費的CPU週期都比較長,通常幾十個CPU週期,而位移運算、位運算只用一個CPU週期。

這樣對於性能要求高的地方,就能夠用位運算代替普通的除法、取餘等運算,JDK源碼中有不少這樣的例子。

爲了可以使用位運算求出這個餘數,length必須是2的N次方,這也是咱們上面初始化數組大小時要求的,而後使用 h & (length-1),就能夠求出餘數。具體的算法推導,請自行搜索。

咱們用個例子來講明下,如一個Key通過運算的hash爲21,length爲16:

直接取餘運算:21 % 16 = 5

位運算:10101(21) & 01111(16-1) = 00101(5)

哇,這就是計算機運算的魅力,這就是算法的做用。

 

另外就是上一步的hash算法,由於咱們求Key所在數組位置的算法是h & (length-1)

假設下面兩個hash值,:

00000000000000110001001001100010(201314)

00000000000000000000000000100010(34)

若是length=32,則:length-1:

00000000000000000000000000011111(31)

這兩個hash值計算數組位置時,都爲2,其實只有二進制的後六位參與了運算,高位根本沒有任何做用,這樣就加大了產生hash碰撞的機率。

因此上一步的hash算法就是爲了解決這個問題,將hash值的高位進行一系列左移和異或,使高位也參與到與運算裏,上面兩個hash值就能夠分配到不一樣的位置。

addEntry — 添加數據

void addEntry(int hash, K key, V value, int bucketIndex) {
    //若是size大於等於threshold,且數組的這個位置不爲null,則擴容數組
    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);
}

threshold:HashMap實際能夠存儲的Key的個數,若是size大於threshold,說明HashMap已經太飽和了,很是容易發生hash碰撞,致使單向鏈表的產生。

在inflateTable方法中,咱們能夠看到

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

因此這個值是由HashMap的capacity 和負載因子(loadFactor默認:0.75)計算出來的。

loadFactor越小,相同的capacity就更頻繁地擴容,這樣的好處是HashMap會很大,產生hash碰撞的概率就更小,但須要的內存也更多,這就是所謂的空間換時間。

在這裏也注意,擴容時會直接將原來容量乘以2,知足了length爲2的N次方的條件。

createEntry就很少說了,就是將key、value保存到數組相應的位置。

GET

final Entry<K,V> getEntry(Object key) {
    if (size == 0) {
        return null;
    }

     //用和添加時相同的算法求出hash值
    int hash = (key == null) ? 0 : hash(key);
     //直接從數組的響應位置拿到數據,判斷hash相同、key相同,則返回
    for (Entry<K,V> e = table[indexFor(hash, table.length)];
         e != null;
         e = e.next) {
        Object k;
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
            return e;
    }
    return null;
}

獲取時很是簡單,也很是迅速,添加時作的全部工做都是爲快速獲取作的工做。

總結

HashMap是一個很是高效的Key、Value數據結構,GET的時間複雜度爲:O(1) ~ O(n),咱們在使用HashMap時須要注意如下幾點:

1. 聲明HashMap時最好使用帶initialCapacity的構造函數,傳入數據的最大size,能夠避免內部數組resize;

2. 性能要求高的地方,能夠將loadFactor設置的小於默認值0.75,使hash值更分散,用空間換取時間;

相關文章
相關標籤/搜索