Java:HashMap源碼分析(JDK 1.7)

HashMap源碼分析

注:JDK 1.7
首先先總體概括下吧,在1.7中,HashMap是由數組+鏈表的形式組成的(1.8中當HashMap達到一定大小後會使用紅黑樹),具體如下。

/*
數組部分:table 
鏈表部分:Entry<K,V>類含有一個指向Entry<K,V>類對象 的next “指針”
*/
static final Entry<?,?>[] EMPTY_TABLE = {};
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

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

這裏寫圖片描述
對實際存儲方式有一個大致瞭解後就可以從初始化階段講起了。

初始化

在創建HashMap對象時(比如HashMap<String,Object> param = new HashMap<String,Object>())會首先調用如下構造函數,這裏會指定初始化時table(即HashMap)的大小和table(即HashMap)需要進行擴容時的預警大小的比例值。
默認大小static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
擴容預警大小static final float DEFAULT_LOAD_FACTOR = 0.75f;

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

然後會調用另一個構造函數,並且會初始化3個成員變量。
loadFactor 爲判斷HashMap是否需要擴容的預警值大小的比例值,此處爲0.75
threshold 爲HashMap的大小值,此處爲16(2^4),需要擴容的預警值大小 = 0.75 * 16 = 12,
至此,初始化完成。

transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
transient int hashSeed = 0;
private transient Set<Map.Entry<K,V>> entrySet = null;

public HashMap(int initialCapacity, float loadFactor) {
    ......//因爲這裏這段是判斷initialCapacity,
    //loadFactor大小的一些異常處理,所以直接省略
    this.loadFactor = loadFactor;
    threshold = initialCapacity;
    init();
}
/**
* Initialization hook for subclasses. This method is called
* in all constructors and pseudo-constructors (clone, readObject)
* after HashMap has been initialized but before any entries have
* been inserted.  (In the absence of this method, readObject would
* require explicit knowledge of subclasses.)
*/
void init() {
}

創建完了HashMap,就插入一些數據吧。

put(K key, V value) 函數

涉及到的相關源代碼

1,2,3,4
public V put(K key, V value) {
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    if (key == null)
        return putForNullKey(value);
    /*擾動函數,計算key類型的hashcode值後對其進行擾動,
    目的爲配合indexfor函數將值散列的更加均勻*/
    int hash = hash(key);  
    int i = indexFor(hash, table.length); //
    /*計算出傳入的key值應該對應的位置,因爲會出現hash值相同的情況,
    HashMap的處理方式是開放式HashMap,即使用鏈表方式存儲hash值相同時的值,
    這裏就是在查詢當前輸入的key是否已經存在,如果存在的話就覆蓋, 
    並返回之前的value值*/
    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;
        }
    }
    //如果當前輸入的key值在原table中不存在,則進行添加
    modCount++;
    addEntry(hash, key, value, i);
    return null;
}
1.
//初始化大小,擴容預警值大小等等
private void inflateTable(int toSize) {
    // Find a power of 2 >= toSize
    int capacity = roundUpToPowerOf2(toSize);

    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    table = new Entry[capacity];
    initHashSeedAsNeeded(capacity);
}   
2.
//擾動函數,爲了讓key值分佈的更加均勻,分散
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);
}
3.
//計算數組下標值
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);
}
5.
void addEntry(int hash, K key, V value, int bucketIndex) {
    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);
}
6.
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++;
}
static class Entry<K,V> implements Map.Entry<K,V> {
    /**
    * Creates new entry.
    */
    Entry(int h, K k, V v, Entry<K,V> n) {
        value = v;
        next = n;
        key = k;
        hash = h;
    }

1.當table 爲空數組時,執行inflateTable(threshold)函數(threshold此時爲16)。
可以看到inflateTable函數中table = new Entry[capacity];,這裏table變爲了一個大小爲16的Entry數組,並且threshold 又被重新賦值爲擴容預警值大小,這裏爲12。
2.賦值結束後將會計算輸入的key值的hash值(int hash = hash(key)),會使用到key對應類型的hashCode()函數。所以當想要往HashMap中插入自定義類型的key值的話就需要實現一個hashCode()函數。這裏對hashCode返回值進行了一大推的位運算,實際目的是爲了最終在計算數組下標值時儘可能的散列數據。在1.8中優化了hash()函數,將原來的一堆位運算修改爲只進行一次位運算:

static final int hash(Object key){
    int h;
    return (key == null) ?0 : (h = key.hashCode())^(h >>> 16);
}

因爲從1.8的優化代碼比較好理解,實際上這一段代碼與1.7的本質上沒有區別,無非是減少了位運算次數。所以這裏的hash函數用1.8的代碼來講。
我們知道hashCode()函數返回的是int值(public native int hashCode();
Java中int值的大小爲[-2^31,2^31-1],即存儲大小爲32位。(h = key.hashCode())^(h >>> 16)後的值的後16位相當於融合了原來hash值的高16位與低16位特性。然後問題來了這裏如果直接使用hash值,那麼總數會有[-2^31,2^31-1],40億左右的映射控件,但是我們的table(HashMap)初始容量只有16,是裝不下[-2^31,2^31-1]那麼多映射的,所以我們需要對hash值進行取模操作。
這裏寫圖片描述
3.indexFor函數,返回h & (length-1),此時length值爲table.length。可知經過與運算後返回的值的範圍爲[0,length-1]。即將[-2^31,2^31-1]的hash值映射到了[0,length-1]下標值中,可以看出實際上是以hash值的低幾位來決定下標值的,但是如果直接使用hashCode值而不進行擾動操作的話,這低幾位的hashCode值重複干擾的可能性非常大,因此纔會引入擾動函數(具體指hash函數裏的那些位操作)。這樣返回的hash值的低幾位也會包含高几位的特性,一定程度上會降低重複干擾。1.8中修改爲只進行一次位運算來擾動,所以這裏的重複干擾實際也是很拼人品的∠( ᐛ 」∠)_。經過hash的擾動後,可以使得key以更加散列的形式儲存在table中,如果key的hash值重複干擾嚴重的話就會出現大量的鏈表存儲,而鏈表形式的信息和數組形式的信息哪一種查詢起來更方便快捷相信不用多講。
這裏其實也說明了爲什麼HashMap大小一定是2的冪次方。
因爲計算下標映射時的h & (length-1),如果table(HashMap)大小不是2的冪次方,會有部分下標值無法正常使用。

/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

4.當計算出對應的下標值後,就可以開始插入了,等等,如果HashMap中已經存在相同key值的信息呢?所以這裏需要先對錶進行遍歷,查詢是是否存在相同的key值信息。當查詢到有時,就將當前新的value值覆蓋儲存,並返回舊value值。當沒有時,執行插入操作。

5.addEntry(hash, key, value, i)
首先如果table中key-value鍵值對數大於等於threshold值(之前threshold 被重新賦值爲當前table大小擴容預警值係數 = 16 0.75 = 12 = 當前擴容預警大小爲12)並且當前key計算得出的下標值對應的數組下標位置不爲空時,將會進行擴容操作。這部分等下講。先把put操作跑完。

/**
* The number of key-value mappings contained in this map.
*/
transient int size;

6.createEntry(hash, key, value, bucketIndex)
創建新的Enter,舊的Enter放到新的Enter的next下,對照上面給出的源碼。

至此插入完成。

擴容

現在來講下擴容的事,已上面put裏出現的擴容情況說明
相關源代碼

void addEntry(int hash, K key, V value, int bucketIndex) {
    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);
}
void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    //如果現在的大小等於最大容量時,將threshold設置爲最大值,防止之後再次觸發擴容操作
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }
    //新建table存放重新計算位置的值
    Entry[] newTable = new Entry[newCapacity];
    //重新計算之前的值的地址
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
    table = newTable;
    //重新計算擴容預警值大小threshold 32*0.75 24
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Entry<K,V> e : table) {
        while(null != e) {
            Entry<K,V> next = e.next;
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            //重新計算位置
            int i = indexFor(e.hash, newCapacity);
            e.next = newTable[i]; //null
            newTable[i] = e;
            e = next;
        }
    }
}  

可以看到每次擴容的大小爲原來的2倍,這也符合上面提到的table(HashMap)的大小必須爲2的冪次方。
transfer函數會按照新的table大小重新計算之前table中的下標值,並重新存儲。並且這裏可以發現重新計算位置後鏈表中的儲存順序與之前相比變成倒過來了。在1.8中,由於會涉及到紅黑樹,所以對這一塊進行了大量改寫,改寫後的將不會出現1.7中鏈表信息倒過來的這種情況。

get(Object key) 函數

相關源代碼

public V get(Object key) {
    if (key == null)
        return getForNullKey();
    Entry<K,V> entry = getEntry(key);

    return null == entry ? null : entry.getValue();
}

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

    int hash = (key == null) ? 0 : 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;
}

怎麼看怎麼像put的一部分對吧,基本上和put中查重的部分一個樣,如果key值不爲空則計算hash值,然後根據hash值計算出下標值,然後檢索改下標值對應的table數組下標位置處的鏈表,查詢是否存在對應的key值,然後返回,非常簡單。

clear()函數

相關源代碼

public void clear() {
    modCount++;
    Arrays.fill(table, null);
    size = 0;
}

Arrays.class
public static void fill(Object[] a, Object val) {
    for (int i = 0, len = a.length; i < len; i++)
        a[i] = val;
}

很簡單明瞭的全部置空

isEmpty()函數

相關源代碼

public boolean isEmpty() {
    return size == 0;
}

/**
* The number of key-value mappings contained in this map.
*/
transient int size;

remove(Object key)函數

相關源代碼

public V remove(Object key) {
    Entry<K,V> e = removeEntryForKey(key);
    return (e == null ? null : e.value);
}

final Entry<K,V> removeEntryForKey(Object key) {
    if (size == 0) {
        return null;
    }
    //如put中,計算key的hash值,對應的下標值,並暫存對應table下標位置的Entry對象
    int hash = (key == null) ? 0 : hash(key);
    int i = indexFor(hash, table.length);
    Entry<K,V> prev = table[i];
    Entry<K,V> e = prev;

    /*不知道爲什麼在這裏遍歷鏈表時用了while,上面put和get時都是for,
    畫風不一樣了啊(逼死強迫症系列_(:з」∠*)_),
    那我也改成用註釋來說明了∠( ᐛ 」∠)_,皮一下很開心。
    */
    while (e != null) {
        //開始遍歷
        Entry<K,V> next = e.next;
        Object k;
        //當hash相同,key相同時
        if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k)))) {
            //對HashMap修改次數+1
            modCount++;
            //HashMap所存儲的鍵值對-1
            size--;
            //只有在第一次遍歷時prev == e
            if (prev == e)
                table[i] = next;
            else
                prev.next = next;
            e.recordRemoval(this);
            //切斷引用鏈,依賴GC回收,返回該對象
            return e;
        }
        prev = e;
        e = next;
    }

    return e;
}

void recordRemoval(HashMap<K,V> m) {
}

以上基本上包含了我們日常使用中比較常用的HashMap函數,當然HashMap函數一共也沒幾個就是了_(:з」∠*)_

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