HashMap-內部存儲

翻譯自http://coding-geek.com/how-do...
並在原文基礎上做了修改和補充
Java HashMap類實現了Map<K,V> 接口, 這個接口的幾個主要的方法如下:
• V put(K key, V value)
• V get(Object key)
• V remove(Object key)
• Boolean containsKey(Object key)
HashMap用一個內部類去存儲數據Entry<K, V>, 它就是一個簡單的鍵值對,同時有兩個額外的數據:
• 指向另一個entry的指針,有點類似於單向無序鏈表。
• hash值,這個是對應於K的hash 值,以避免每次HashMap使用時重新計算。
在JDK 7中,Entry的部分實現如下:
static class Entry<K,V> implements Map.Entry<K,V> {

    final K key;
    V value;
    Entry<K,V> next;
    int hash;

}
HashMap將數據存儲到多個Entry鏈表,然後每個鏈表都會註冊到Entry數組中去,對應於一個bucket。這初始數組的大小是16(DEFAULT_INITIAL_CAPACITY)

HashMap中有一個桶(bucket)的概念, 其實這個桶就是entry數組(HashMap初始化的時候會創建一個固定大小的數組)中的一個元素,如下所示:
/**

 * The table, resized as necessary. Length MUST Always be a power of two.
 */
transient Entry[] table;

有相同hash value的key都會被放到同一個桶中,然後entry之間通過指針相連,就組成了鏈表。當調用HashMap的put或者是get方法時,方法首先計算key的hash值,然後計算出到底是該屬於哪個bucket。找到桶之後,遍歷bucket對應的entry 鏈表,再找到對應的key值,鏈表裏面的查是通過key的equals方法完成的。該步驟的代碼如下所示(JDK 7):
public V get(Object key) {

    if (key == null)
        return getForNullKey();
    int hash = hash(key.hashCode());
    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.equals(k)))
            return e.value;
    }
    return null;
}

map 生成桶需要經過下面的3步:
• 首先獲取key的hash value。
• 然後會再次計算一次hash, 這樣做的目的是爲了避免hash函數將數據都放在同一個桶中(以下是JDK 7的實現,JDK 8做了簡化,原理一樣)。
/**

 * Applies a supplemental hash function to a given hashCode, which
 * defends against poor quality hash functions.  This is critical
 * because HashMap uses power-of-two length hash tables, that
 * otherwise encounter collisions for hashCodes that do not differ
 * in lower bits. Note: Null keys always map to hash 0, thus index 0.
 */
static int hash(int h) {
    // 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 會提供這樣的一個補充函數,它作用於HashCode, 就是爲了防止不穩定的hash函數造成的衝突。該函數稱爲‘擾動函數‘。
• 拿到重新計算的hash值之後, 再計算它對應的index,也就是該放到哪個桶裏面。
/**

 * Returns index for hash code h.
 */
static int indexFor(int h, int length) {
    return h & (length-1);
}

爲什麼需要重新計算hash?舉個栗子:
shinadata字符串的 HashCode的binary是 1000001111000010101000110010001,上述代碼中的length 就是數組(Entry[] table)的長度。(length – 1)=15,15的二進制是1111,如果不做rehash操作,此時1111高位補0,和1000001111000010101000110010001 做位與&運算,那麼shinadata 的HashCode 在計算過程中只有最後幾位能起作用,無論高位是多少,只要低位是相同的話,那麼最後的結果就相同了,這樣indexFor方法計算出的index值就容易產生衝突。擾動函數的作用就是爲了消除這方面的影響。
爲什麼內部數組的大小是2的冪?
假設數組大小是17,那麼掩碼值是16(size -1),二進制是0…010000, 現在對於任何哈希值h , h & 16 得到索引不是16就是0。那麼這就意味着,數組大小17,使用的桶只能是2個。但是要是用2的冪的值的話, 那就不同了,比如16,size - 1 = 15, 二進制是0…001111, h & 15 得到的索引值可以使 0~15。
這部分可以查看JDK 7的源碼。http://hg.openjdk.java.net/jd...
參考:
https://blog.csdn.net/wenyiqi...
https://www.zhihu.com/questio...
翻譯自http://coding-geek.com/how-do...
並在原文基礎上做了修改和補充

clipboard.png

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