沒想到你是這樣HashMap!!

沒想到你是這樣HashMap!!

相信你們在面試的過程當中,常常會被問到一個這樣的問題:"你瞭解hashmap的底層原理嗎?",大多數初級人員或許只是瞭解它的底層數據結構是什麼,基本的做用是什麼,可是一旦問到擴容過程,put的過程,紅黑樹的變色和翻轉(本篇不支持),你們不免就沒法從容面對。node

那咱們就一塊去看下hashmap的底層源碼是什麼樣子的~~面試

3d4698abfd68423649b6ba2f9c24a9f3.jpeg


開始以前請你們思考一下,咱們常見的數據結構有哪些?典型的表明又有哪些呢?算法

01 常見數據機構數組

咱們比較熟悉的應該是這幾種:數組,鏈表(單向和雙向),樹形,圖形數據結構

典型的表明:app

數組:相似以下,典型表明是Arraylist和Vectorless

aa269a4e84170fc3d97e9b4c3f69bcfb.jpeg


鏈表:典型表明是LinkedListide

雙向:this


2d3ed9755eda41381ed5886702945529.jpeg

單向3d

a47265b96b5f753326f5cbca89b98882.jpeg

紅黑樹:典型表明是hashmap(jdk1.8以前是數組加鏈表,1.8以後又增長了紅黑樹)


07bdddf80e4368aeb37b54a7e1332d27.jpeg


hashmap的結構組成是 :數組+鏈表+紅黑樹


02  HashMap的常見參數


咱們能夠先考慮這麼幾個問題:


  • 既然hashmap的底層結構含有數組,那麼咱們應該知道,數組是要指定一個大小的,那麼默認大小是多少呢?
/** * The default initial capacity - MUST be a power of two. */static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

DEFAULT_INITIAL_CAPACITY參數就是默認的大小值,就是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;

MAXIMUM_CAPACITY就是數組的上限值

  • 默認長度是16,這個長度是有時知足不了咱們的業務需求的,這也就意味着咱們要擴容,不少人可能以爲只要數組長度達到16就開始擴容,俗話說未雨綢繆,既然知道存在長度可能不夠的狀況,那咱們就要提早作準備纔是!
/** * The load factor used when none specified in constructor. */static final float DEFAULT_LOAD_FACTOR = 0.75f;

DEFAULT_LOAD_FACTOR加載因子,也就是說當容器使用了16*0.75=12,的時候,就開始擴容。

d104da66ddf309fd68889dd812587487.jpeg


  • jdk8引入了紅黑樹,緣由天然是由於紅黑樹擁有更高的效率,可是並無拋棄掉鏈表。爲何呢?任何東西,存在即合理,紅黑樹在必定程度上比鏈表更高效,可是有時候鏈表更有優點!


這也就意味着它們之間能夠進行轉換,根據不一樣的場景選擇不一樣的存儲結構,,這纔是合理的,那麼何時纔會轉換呢?


鏈表轉紅黑樹:

/** * 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;

根據英文的意思就能夠看出這是用於紅黑樹的,意味着當鏈表的長度>=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;

UNTREEIFY_THRESHOLD 紅黑樹轉鏈表,即樹的深度<=6的時候,會轉化爲鏈表。


轉換就意味着會產生衝突,爲了不衝突,咱們還須要可以成爲樹的最小數量

/** * The smallest table capacity for which bins may be treeified. * (Otherwise the table is resized if too many nodes in a bin.) * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts * between resizing and treeification thresholds. */static final int MIN_TREEIFY_CAPACITY = 64;

MIN_TREEIFY_CAPACITY,是樹的節點最小數量,依據就是 4 * 數組長度(16)=64


03put的過程




a0f59ce4731c97743d3fb85b46101200.jpeg


明白了上面幾個參數的意義就是,咱們就開始嘗試去看一下源碼,咱們就以put方法爲例子,看看究竟是怎麼個意思。

5f5716f70f390fdc50baffebc9c3afff.jpeg

咱們看到 putVal(hash(key), key, value, false, true);

這有4個參數,前兩個分別是key(key的hash值),value,第三個表明遇到重複值是否要覆蓋,fasle是覆蓋,最後一個參數是指插入結束後要不要建立新的模式,false表明是(可參考英文註解)


此時你或許會疑問,爲何要對key進行hash化,這涉及到hash算法,你們想一下,map的數組長度默認是16,並且map是無序的,也就意味着要在下標爲0-15中隨機產生並且人家更要考慮的均勻性,就是0-15不只要隨機,還要每一個數字都要雨露均沾。目的就是儘可能讓這些數字產生的更加公平均勻。

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

至於如何雨露均沾,日後看。

下面我會摘取源碼的一塊塊的進行截取解讀,建議你們比着源碼來看

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;

看一下前幾行的代碼,首先定義了兩個數組tab和p,以及兩個int變量,n和i,咱們繼續往下看就知道做用了。

if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;

table是常量,記錄了map的數據,咱們每插入一條數據,table就會多一條記錄下來。


這塊代碼就是初始化,看看是否是第一次添加數據,若是是就經過resize()方法給tab初始化,並由變量n記錄當前長度,此時咱們已經看到了兩個變量的做用。


Resize()方法的做用有2個,初始化數組和擴容,最後咱們會一塊兒看一下,此處先認識有這樣一個操做 。

if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);else {

數組下標值是(n -1) & hash  這行代碼來獲取的,爲何能實現雨露均沾,這個就涉及二進制的&運算,具體請關注往期文章

else後面的代碼我並無粘過來,爲何?由於這就涉及到一個常見面試點:hash碰撞

什麼是hash碰撞?就是產生的hash值重複了,既然是產生1-16,那麼重複的機率仍是很高的,沒有重複,就按照上面所寫的,我新建一個節點就行了,可是重複了呢?就是else的內容了,在此以前咱們先明確思路,再去看代碼就簡單的很了

e6bde6faae20fbd6c2be439363eb133f.jpeg

1,要追加的地方,自己尚未鏈表,要添加的是第一個

2,我要追加的可能不是鏈表,多是紅黑樹,那我直接轉變成樹的節點就好

3,後面有鏈表,那就須要我不停的去遍歷,而後找到合適的位置,可是由於是新添加節點,咱們還要考慮鏈表長度達到了8,就要轉變成紅黑樹。

else {
    Node<K,V> e; K k;//狀況1
    if (p.hash == hash &&
        ((k = p.key) == key || (key != null && key.equals(k))))
        e = p;//狀況2  轉變爲樹的節點
    else if (p instanceof TreeNode)
        e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);//狀況3  循環查找鏈表查找位置
    else {
        for (int binCount = 0; ; ++binCount) {
            if ((e = p.next) == null) {
                p.next = newNode(hash, key, value, null);
    //若是長度大於等於8了,要變成紅黑樹
                if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                    treeifyBin(tab, hash);
                break;
            }
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                break;
            p = e;
        }
    }//發現重複,替換掉
    if (e != null) { // existing mapping for key
        V oldValue = e.value;
        if (!onlyIfAbsent || oldValue == null)
            e.value = value;
        afterNodeAccess(e);
        return oldValue;
    }}++modCount;if (++size > threshold)
    resize();afterNodeInsertion(evict);return null;

不知道根據上面的註釋你們是否有了理解

++modCount;if (++size > threshold)
    resize();

最後幾行,由於咱們隨時要記錄長度,準備擴容,最後幾行的目的就是來判斷是否要擴容的.。

641a4831e86b632cf8d27adce7255bed.jpeg

擴容,resize()也是常常被問到的,咱們也去看下,仍是先說思路;

1,擴容就是達到了指定長度後,每次擴容2倍,16會變成32,可是若是容器自己超過了最大限制,就無法擴容了

2,擴容後會對現有數據從新排序,爲何呢?和生活同樣,咱們住的空間大了,那咱們的行李用品也要搬一些到新空間,別顯得那麼擁擠,但又不是所有搬走,會選一部分搬,至於怎麼選呢?就體現出二進制算法的精妙之處了,具體如何實現,日後看

Node<K,V>[] oldTab = table;int oldCap = (oldTab == null) ? 0 : oldTab.length;int oldThr = threshold;int newCap, newThr = 0;//oldcap爲0說明是初始化,不要忘記resize的做用是初始化和擴容if (oldCap > 0) {
   //長度超出了最大值,沒法擴容
    if (oldCap >= MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return oldTab;
    }//擴容後的值不超過最大值,就擴容2倍
    else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
             oldCap >= DEFAULT_INITIAL_CAPACITY)
        newThr = oldThr << 1; // double threshold}//初始化操做else if (oldThr > 0) // initial capacity was placed in threshold
    newCap = oldThr;else {               // zero initial threshold signifies using defaults//從新計算加載因子等
    newCap = DEFAULT_INITIAL_CAPACITY;
    newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}if (newThr == 0) {
    float ft = (float)newCap * loadFactor;
    newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
              (int)ft : Integer.MAX_VALUE);}threshold = newThr;

剛纔也提到擴容後從新排序的,感興趣的本身去研究一下,下面是代碼

if (oldTab != null) {
    for (int j = 0; j < oldCap; ++j) {
        Node<K,V> e;
        if ((e = oldTab[j]) != null) {
            oldTab[j] = null;
            if (e.next == null)
                newTab[e.hash & (newCap - 1)] = e;
            else if (e instanceof TreeNode)
                ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
            else { // preserve order
                Node<K,V> loHead = null, loTail = null;
                Node<K,V> hiHead = null, hiTail = null;
                Node<K,V> next;
                do {
                    next = e.next;
                    if ((e.hash & oldCap) == 0) {
                        if (loTail == null)
                            loHead = e;
                        else
                            loTail.next = e;
                        loTail = e;
                    }
                    else {
                        if (hiTail == null)
                            hiHead = e;
                        else
                            hiTail.next = e;
                        hiTail = e;
                    }
                } while ((e = next) != null);
                if (loTail != null) {
                    loTail.next = null;
                    newTab[j] = loHead;
                }
                if (hiTail != null) {
                    hiTail.next = null;
                    newTab[j + oldCap] = hiHead;
                }
            }
        }
    }}return newTab;

可是要提醒的是,剛纔說到得巧妙之處是

if ((e.hash & oldCap) == 0

此處的&運算就是精密之處,經過二進制運算來比較現有的與原來的不一樣,會產生0和1,0就留下,1就移走,移走後的位置下標是  原先位置+擴容長度


纔能有限,源碼就帶你們看到這,那咱們進入最後一個環節,問幾個問題,看看可否回答?

c22d38155136f6bd669cd88861c119c1.jpeg

1,hashmap的底層原理,數據結構是什麼?

底層是哈希表(數組加鏈表),1.8以後引入了紅黑樹

2,能說一下hashmap的put過程嗎?

1>傳入key-value,並根據key求出哈希值,用於計算下標2>查看是否衝突,不衝突就裝入容器中,3>衝突就追加到鏈表中,而且要查看是否達到鏈表閾值,達到要轉換成紅黑樹4>查看節點是否重複,重複就覆蓋5>查看容器是否要擴容

3,hashmap是如何得到下標的?

(n-1)  & hash.  原理是高16位不變,與低16位作與或運算

4,能說下擴容嗎?擴容後部分數組位置確定要變化,變成什麼了呢?

1>判斷當前容量或擴容後的容量是否超出最大值,超出則沒法擴容,不然擴容會增長2倍2>從新遍歷數組,而後將部分移動到新位置(注意:resize還有初始化操做,若是記錄的table常量是空就初始化)位置就是原有位置+擴容數量

5,hashmap中的鏈表太長,查找時間複雜度可能會達到0(n),如何解決?

引入紅黑樹就是爲了解決這個問題


原做者:歸行-泰然
原文連接: 沒想到你是這樣HashMap!!
原出處:公衆號
侵刪

b30a8a37452e6e5b316070f3ef596f4e.jpeg

相關文章
相關標籤/搜索