JDK1.8源碼逐字逐句帶你理解HashMap底層(1)

引言:

自己在學習這個東西的時候,發現網上很多關於HashMap底層介紹的文章基於的jdk版本比較低。因爲我對比之後發現編碼風格有了比較大的改變。而且,今天我想嘗試一種很通俗的方式來嘗試記錄這次的學習。在本文中我主要整理了HashMap類的重要成員變量和關鍵方法的涵義和作用,HashMap初始化方式並描述初始化變量。瞭解HashMap存儲結構,根據JDK源碼逐字逐句解讀核心方法。筆者目前整理的一些blog針對面試都是超高頻出現的。大家可以點擊鏈接:http://blog.csdn.net/u012403290

技術點:

1、數組與鏈表

簡單通俗來說,兩者各有優劣。對於數組來說,它的存儲空間是連續的,佔用內存嚴重,連續的大內存進入老年代的可能性也會變大(關於GC後面我會把學習的也記錄下來),但是正因爲如此,尋址就顯得簡單,也就是說查詢某個arr會有指定的下標,但是插入和刪除比較困難,因爲每次插入和刪除時,如果數組在插入這個地方後面還有很多數據,那就要後面的數據整體往前或者往後移動。對於鏈表來說存儲空間是不連續的,佔用內存比較寬鬆,它的基本結構是一個節點(node)都會包含下一個節點的信息(如果是雙向鏈表會存在兩個信息一個指向上一個一個指向下一個),正因爲如此尋址就會變得比較困難,插入和刪除就顯得容易,鏈表插入和刪除的時候只需要修改節點指向信息就可以了。

2、哈希表/散列表(Hash table 注意這個不是JAVA線程安全類:HashTable)

你有故事我有酒”,很多時候兩者結合才顯得韻味十足。在哈希表的結構中就融入了數組和鏈表的結構,從而產生了一種尋址容易,插入刪除也容易的新存儲結構,下圖是百度百科引入的圖片:

這裏寫圖片描述

其實哈希表的實現方式有很多種,我們就研究如上這最常用的一種:

爲了解釋方便,我們定義兩個東西:String[] arr; 和 List list;
那麼,上圖左邊那一列就是arr, 就是整個的arr[0]~arr[15],且arr.length() = 16。上圖每一行就是一個list,這裏理論來說應該最大存儲16個List。每個數組存存放的應該是某一個鏈表的頭,也就是arr[0] == list.get(0)。不知道我這樣的描述是否清楚。

那麼如何確定某一個對象是屬於數組的某個下標呢?一般算法就是 下標 = hash(key)%length。算式中的key是存放的對象,hash這個對象會得到一個int值,這個int值就是在上圖中所體現的數字,length就是這個數組的長度。我們用上圖中的arr[1]打個比方,1%16 =1,337%16 = 1, 353%16 =1。大家就存儲在arr[1]中。

HashMap詳解

主要成員變量和方法

  • loadFactor:稱爲裝載因子,主要控制空間利用率和衝突。大致記住裝載因子越大空間利用率更高,但是衝突可能也會變大,反之則相反。源碼中默認0.75f。
   /**
     * The load factor used when none specified in constructor.
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
  • DEFAULT_INITIAL_CAPACITY與MAXIMUM_CAPACITY:稱爲容量,用於控制HashMap大小的,下面是源碼中的解釋:

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

    /**
     * The maximum capacity, used if a higher value is implicitly specified
     * by either of the constructors with arguments.
     * MUST be a power of two <= 1<<30.
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;
  • THRESHOLD: 這個字段主要是用於當HashMap的size大於它的時候,需要觸發resize()方法進行擴容。下面是源碼:
    /**
     * The bin count threshold for using a tree rather than list for a
     * bin.  Bins are converted to trees when adding an element to a
     * bin with at least this many nodes. The value must be greater
     * than 2 and should be at least 8 to mesh with assumptions in
     * tree removal about conversion back to plain bins upon
     * shrinkage.
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * The bin count threshold for untreeifying a (split) bin during a
     * resize operation. Should be less than TREEIFY_THRESHOLD, and at
     * most 6 to mesh with shrinkage detection under removal.
     */
    static final int UNTREEIFY_THRESHOLD = 6;

看到這裏,也許一部分人心中會有一個疑問,爲什麼前面賦值要用移位的方式,而這裏就直接賦值8而不用1<<3呢?注意一個點,在上面有一句解釋:“ MUST be a power of two.”,意思就是說這個變量如果發生變化將會是以2的冪次擴容的。比如說1<<4進行擴容一位的話就是1<<4<1那結果就是32啦。

關於這幾個參數的使用和關係請參考下面的HashMap初始化和源碼分析。

初始化:

筆者寫了一個小Demo:

package com.brickworkers;

import java.util.HashMap;

public class HashMapTest {

    public static void main(String[] args) {

        HashMap<String, String> map = new HashMap<String, String>();
        for(int i = 0; i<1000; i++){
//          map.entrySet();
//          map.keySet();
//          map.values();
            map.put(String.valueOf(i), String.valueOf(i));
        }
    }
}

在逐步debug的過程中,發現new了一個HashMap的時候,map中的初始化情況是這樣的:
這裏寫圖片描述

然後等我put一個鍵值對進入的時候就會變成這樣:

這裏寫圖片描述

從這個之間的變化,我們發現在new了一個新的hashMap的時候並沒有對所有的成員變量進行賦值。當觸發了put操作之後就開始變化了。
接着,我們點開table,table主要是鍵值對數組,也就是存在HashMap中真真實實存的值:
這裏寫圖片描述

發現table是一個長度爲16的數組。在這裏我們總結一下。table.length爲16, threshold:12,loadFactor:0.75。是不是發現table.lenth = threshold/loadFactor呢?前面有說道capacity這個變量,其實這個變量就是table.length,但是大家要區分好capacity和size的區別。上圖中的size是HashMap中已存在存儲對象的數量。

接着,我們繼續研究,前面提到,觸發擴容(resize()方法)是數據的size>rhreshold的時候,那麼我們就debug到擴容階段:

觸發擴容

擴容成功,發現對應的threshold變成了24,table.length = 32。我們驗證一下前面的算式對不對:24/0.75 =32。還有一個有趣的一點,table中的數組存放順序是這樣的(仔細看上圖應該也能看出):[11=11, 12=12, null, null, null, null, null, null, null, null, null, null, null, null, null, null, 0=0, 1=1, 2=2, 3=3, 4=4, 5=5, 6=6, 7=7, 8=8, 9=9, null, null, null, null, null, 10=10],是不是覺得很有意思?

有的小夥伴又有疑問了,那麼那個一直在變化的modCount又是什麼東西呢?這個東西其實是滲透在HashMap類中的方法裏面的,只要HashMap發生一次變化,就會對應的+1,可以稱爲修改次數計數器。它主要是存在與非線程安全的集合當中。我們知道HashMap是一種非線程安全的類(這裏涉及到一個HashMap與HashTable的區別),那麼如果某一個HashMap對象你在操作的過程中,被別的線程修該了怎麼辦?那不是亂套了麼?因此,modCount就應運而生啦,在迭代器初始化過程中會將modCount值賦的給迭代器的 expectedModCount。在迭代過程中,判斷 modCount 跟 expectedModCount 是否相等,如果不相等就表示已經有其他線程修改了 Map,從而拋出ConcurrentModificationException。這就是fail-fast策略。不知道筆者這樣描述大家能否理解吶?

對於那些看到這裏還興趣盎然的小夥伴們說一聲道歉,筆者本來打算今天把所有的一次性都寫完,但是還有一些事情和工作需要去完成,所以呀,寫到這裏我上去給標題最後面加了個(1)。關於HashMap的逐字逐句介紹源碼底層實現和一些關鍵方法put(),get(),resize()等等只有下一篇博文再把自己學到的一些皮毛展示給大家哦。

下面是我的微信二維碼,加個好友沒事可以聊聊天:
這裏寫圖片描述

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