從根源揭祕HashMap的數據存儲過程



類型 描述 用時
選題 silencezwm 0.1小時
寫做時間 2017年12月3日 5小時
審稿 silencezwm 0.5小時
校對上線 silencezwm 0.1小時

Tips:4個環節,共計約5.7小時的精心打磨完成上線。html


在咱們平常的開發過程當中,HashMap的使用率仍是很是高的。本文將首先對Map接口的基本屬性和方法作一個簡單的介紹,而後從HashMap的初始化、增長數據兩方面來進行探討。java

經過本文的學習,你能夠了解到:android

1、Map接口的簡單介紹數組

2、HashMap的初始化過程函數

3、HashMap的增長數據過程學習


1、Map接口的簡單介紹

咱們查看Map源碼,可知道Map是以key-value(鍵值對)形式存在的接口,由其衍生出來的接口和類也是至關多的,好比今天的主角HashMap,還有TreeMap、Hashtable、SortedMap等等。this

其經常使用的方法以及描述以下:spa

方法 描述
V put(K key, V value) 往Map中存入一個鍵值對數據,並返回一個Value
void putAll (Map<? extends K, ? extends V> map) 往Map中存入一個Map數據
V remove (Object key) 根據key刪除該數據,並返回該Value
void clear () 清空Map現有數據
V get (Object key) 根據key查詢對應的Value
boolean isEmpty () 判斷Map是否爲空
int size () 返回Map存有數據的個數
boolean containsKey (Object key) 判斷Map是否包含該key
boolean containsValue (Object value) 判斷Map是否包含該value

關於Map的更多介紹,可參閱Api文檔.net


2、HashMap的初始化過程

首先咱們來看下HashMap的繼承以及接口實現關係:code

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
複製代碼

AbstractMap一樣也實現了Map接口。因此,HashMap擁有Map全部的特徵也是毋庸置疑的。而且HashMap的靜態內部類HashMapEntry<K,V>也實現了Map.Entry<K,V>接口,以下:

static class HashMapEntry<K,V> implements Map.Entry<K,V> {
    final K key;
    V value;
    HashMapEntry<K,V> next;
    int hash;

    HashMapEntry(int h, K k, V v, HashMapEntry<K,V> n) {
        value = v;
        next = n;
        key = k;
        hash = h;
    }
    
    ......
}
複製代碼

HashMap的表中存放的每個數據都是HashMapEntry<K,V>的一個對象,其包含key、value、指向下一個對象的引用對象next以及該key生成的哈希碼值。

咱們先來看看HashMap幾個重要的全局變量

// HashMap的初始容量
static final int DEFAULT_INITIAL_CAPACITY = 4;

// HashMap的最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;

// 在構造函數中沒有指定的加載因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;

// HashMap未初始化時的數組空表
static final HashMapEntry<?,?>[] EMPTY_TABLE = {};

// 該反序列化數組table在HashMap須要調整容量時使用,默認爲空表
transient HashMapEntry<K,V>[] table = (HashMapEntry<K,V>[]) EMPTY_TABLE;

// HashMap的大小
transient int size;

// 該值用於HashMap須要調整容量時使用
int threshold;

// 加載因子,默認爲0.75f
final float loadFactor = DEFAULT_LOAD_FACTOR;

// 計數器
transient int modCount;
複製代碼

HashMap的構造方法有:

方法 描述
HashMap() 獲得一個新的空HashMap實例
HashMap(int capacity) 根據傳入的容量實例化空HashMap
HashMap(int capacity, float loadFactor) 根據傳入的容量、加載因子實例化空HashMap
HashMap(Map<? extends K, ? extends V> map) 傳入已有Map對象實例化新的HashMap

這裏就選擇第一個構造方法來探討,其代碼以下:

public HashMap() {
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}

public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY) {
        initialCapacity = MAXIMUM_CAPACITY;
    } else if (initialCapacity < DEFAULT_INITIAL_CAPACITY) {
        initialCapacity = DEFAULT_INITIAL_CAPACITY;
    }

    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
    
    threshold = initialCapacity;
    init();
}
複製代碼

從默認的構造方法中能夠看出,有 initialCapacity(初始容量) 和 loadFactor(加載因子) 這兩個參數。由於咱們並無經過其餘構造方法傳入這兩個參數,因此其就會使用默認值。

該構造方法使用流程圖表示以下:

構造方法流程圖

因此,整個初始化過程僅僅就是對參數的合理性進行判斷以及肯定幾個變量的初始值。

3、HashMap的增長數據過程

既然咱們有了HashMap的實例,那就能夠往裏存放數據了,而其存放數據用到的方法是:

public V put(K key, V value) {
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    if (key == null)
        return =;
    int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);
    int i = indexFor(hash, table.length);
    for (HashMapEntry<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++;
    addEntry(hash, key, value, i);
    return null;
}
複製代碼

該put方法的整個流程解析以下:

一、表的初始化:咱們剛在構造方法中,並無對table進行初始化,因此inflateTable方法會被執行;

private void inflateTable(int toSize) {
    int capacity = roundUpToPowerOf2(toSize);

    float thresholdFloat = capacity * loadFactor;
    if (thresholdFloat > MAXIMUM_CAPACITY + 1) {
        thresholdFloat = MAXIMUM_CAPACITY + 1;
    }

    threshold = (int) thresholdFloat;
    table = new HashMapEntry[capacity];
}

private static int roundUpToPowerOf2(int number) {
    int rounded = number >= MAXIMUM_CAPACITY
            ? MAXIMUM_CAPACITY
            : (rounded = Integer.highestOneBit(number)) != 0
                ? (Integer.bitCount(number) > 1) ? rounded << 1 : rounded
                : 1;

    return rounded;
}
複製代碼

roundUpToPowerOf2方法的做用是用來返回大於等於最接近number的2的冪數,最後對table進行初始化。

二、根據key存放數據:這裏分 「key爲null」 和 「key不爲null」 兩種狀況處理。

狀況一:key爲null

此種狀況將會調用putForNullKey方法,

private V putForNullKey(V value) {
    for (HashMapEntry<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++;
    addEntry(0, null, value, 0);
    return null;
}
複製代碼

首先對數組table從頭至尾遍歷,當找到有key爲null的地方,就將舊值替換爲新值,並返回舊值。不然,計數器modCount加1,調用addEntry方法,並返回null。

狀況二:key不爲null

此種狀況首先會根據indexFor(hash, table.length)生成的bucketIndex去table中查找是否存在相同bucketIndex的value,若是有,就將舊值替換爲新值,並返回舊值。不然,計數器modCount加1,調用addEntry方法,並返回null。

以上兩種狀況最終都指向了addEntry方法,來看看其具體實現:

void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {
        resize(2 * table.length);
        hash = (null != key) ? sun.misc.Hashing.singleWordWangJenkinsHash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }

    createEntry(hash, key, value, bucketIndex);
}
複製代碼

該方法中,首先判斷table是否須要擴容。若是須要擴容,則執行resize方法,傳入的參數爲現有table長度的兩倍。

void resize(int newCapacity) {
    HashMapEntry[] oldTable = table;
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }

    HashMapEntry[] newTable = new HashMapEntry[newCapacity];
    transfer(newTable);
    table = newTable;
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
複製代碼

resize方法中,若是表容量已經達到最大值,則直接返回Integer.MAX_VALUE。不然根據新的容量值建立新表,並執行數據遷移方法transfer。

void transfer(HashMapEntry[] newTable) {
    int newCapacity = newTable.length;
    for (HashMapEntry<K,V> e : table) {
        while(null != e) {
            HashMapEntry<K,V> next = e.next;
            int i = indexFor(e.hash, newCapacity);
            e.next = newTable[i];
            newTable[i] = e;
            e = next;
        }
    }
}
複製代碼

transfer方法的做用就是將老表的數據所有遷移到新表中。

void createEntry(int hash, K key, V value, int bucketIndex) {
    HashMapEntry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new HashMapEntry<>(hash, key, value, e);
    size++;
}
複製代碼

最後將數據添加到table的bucketIndex位置,並將size加1。

如今,用兩個小圖來表示put過程的兩種狀態,以下:

put方法狀況一
put方法狀況二

其中數據存放的位置bucketIndex是由 key 和 表的長度 共同決定的。在addEntry方法中計算獲得:

bucketIndex = indexFor(hash, table.length);
複製代碼

因此有可能會出現bucketIndex相同的狀況,也稱之爲bucketIndex碰撞,當碰撞發生時,相同bucketIndex的value會經過單鏈的形式鏈接在一塊兒,此時HashMapEntry<K,V>中的next就會指向下一個元素。也就印證瞭如下這句話:

若是hashCode不一樣,equals必定爲false;若是hashCode相同,equals不必定爲true。


最後,預祝你學習愉快!

把文章分享出去吧


相關文章
相關標籤/搜索