從JDK源碼分析HashMap

筆者只是一個大三期末慌着找實習、工做一枚渣渣,第一次正式開始總結JAVA基礎知識(面試中很須要的啊),若是講得有錯的地方還請讀者們多多包涵並狠狠地在評論區懟出來哈~【因爲不少內容借鑑於互聯網的精彩博文,故而歡迎你們轉載】html

前言:面試

對於如何快速學習一門語言,除了一遇到問題就GOOGLE或者問度娘以外,還要注意培養本身動手,獨立解決問題的能力。(筆者這個ZZ話在嘴上講,ACDEF數心中留)那麼官方API文檔以及JDK自己自帶的源碼包(就是安裝的JDK目錄下的src.zip)都是很是好的自學工具。數組

下面先說說如何使用IDE查看JDK源碼,以筆者如今使用的IDE(IntelliJ IDEA)爲例,能夠新建一個project(專門存放JDK的src.zip源碼包工程,方便之後學習查看),在圖中<10>(筆者如今是查看JDK 10的包,雖然項目中仍是用JDK 1.8【尷尬】)目錄下,最後結果是中的src.zip,以後右鍵選擇設置爲library root,就能夠查看內容了緩存

正文:安全

在Java.base中找HashMap的class,如圖數據結構

點開後,首先查看到上方的綠色註釋【概述了該類或者接口的主要狀況】:併發

只截圖了一部分,app

稍做總結以下:工具

1.容許key value爲null性能

2.大體跟HashTable相同,除了不保證同步和容許null;想要同步建議使用

Map m = Collections.synchronizedMap(new HashMap(...));

我的認爲concurrentHashMap也能夠啊,不過前者能夠接受任何種類Map實例,但後者只能是HashMap實例,具體細節(參考大佬)以下:

Collections.synchronizedMap()和Hashtable同樣,實現上在調用map全部方法時,都對整個map進行同步,而ConcurrentHashMap的實現卻更加精細,它對map中的全部桶加了鎖。因此,只要要有一個線程訪問map,其餘線程就沒法進入map,而若是一個線程在訪問ConcurrentHashMap某個桶時,其餘線程,仍然能夠對map執行某些操做。這樣,ConcurrentHashMap在性能以及安全性方面,明顯比Collections.synchronizedMap()更加有優點。同時,同步操做精確控制到桶,因此,即便在遍歷map時,其餘線程試圖對map進行數據修改,也不會拋出ConcurrentModificationException。不論Collections.synchronizedMap()仍是ConcurrentHashMap對map同步的原子操做都是做用的map的方法上,map在讀取與清空之間,線程間是不一樣步的

3.初始容量太高或裝載因子太低會形成遍歷效率低下【重要】

4.若 初始容量*裝載因子<哈希表的容量 ,則哈希表進行散列-->buckets*2

故考慮好初始容量和裝載因子設定,儘可能使結果接近並稍大於哈希表,即避免散列,提升效率

5.裝載因子=0.75(默認)

6.迭代器:兩種迭代方式Map map = new HashMap();  [1] Set set = map.keySet();

String key = (String)iter.next();  //鍵            String value = (String)map.get(key);//值

             [2]Set set = map.entrySet();  Map.Entry entry =     (Map.Entry)iter.next();//【鍵+值】

引用大佬知識

1.HashMap 最底層依然是數組來實現的,咱們想HashMap中所放置的對象其實是存儲在該數組當中的
2.當向HashMap中put一對鍵值時,它會根據key的hashcode值計算出一個位置,該位置就是此對象準備往數組中存放的位置
3.若是該位置沒有對象存在,就將對象直接放進數組當中;若是該位置已經有對象存在了,則順着此存在的對象的鏈開始尋找(Entry  類有一個Entry類型的next成員變量,指向了該對象的下一個對象),若是此鏈上有對象的話,再去使用equal方法進行比較,若是對此鏈上的某個對象的equals方法比較爲false,則將該對象放到數組當中,該位置之前存在的那個對象連接到此對象的後面

以後會在JDK源碼中找到實現,以做解釋

現先繼續從頭分析源碼:

初始容量=16

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

最大容量=2*1<<30(左移,至關於2*1的30次方)

static final int MAXIMUM_CAPACITY = 1 << 30;

裝載因子=0.75(默認)

static final float DEFAULT_LOAD_FACTOR = 0.75f;

桶轉換爲樹(紅黑樹)的閾值最小=8【後面會繼續解釋相關】

static final int TREEIFY_THRESHOLD = 8;

樹轉換爲桶的閾值最大=6

static final int UNTREEIFY_THRESHOLD = 6;

桶轉換爲樹的最小容量=64(後面有用到)

static final int MIN_TREEIFY_CAPACITY = 64;

查當作員屬性:方法以下圖示:

內部類Node,實際是一個鏈表

還有put()方法:

其中putVal()說明存進去的數據就是Node的鏈表解構,放在了一個數組中,故而證明HashMap底層就是鏈表+數組-->散列表

看下HashMap的構造方法(有四個):

每一個都看一下

1.判斷初始大小是否合理、賦值初始化初始大小、判斷裝載因子、賦值初始化裝載因子

其中在1.中有

this.threshold = tableSizeFor(initialCapacity);

tableSizeFor()是爲初始化閾值,threshold 決定了是否要將散列表再散列。

選出最小的2的N次方數值作閾值(跟紅黑樹有關)

下面是查看怎麼計算Hash的:

>>>無符號右移,忽略符號位,空位都以0補齊

以前因爲設定初始大小爲16,那麼在給put進的數找位置就是根據hash值來找的,故而必須保證hash不衝突,key的hashCode也要與h的右移16位進行異或運算,下降hash碰撞衝突的機率

另外,上圖中分析:1.若要插入的key和hash都相等,記錄->e到桶中

2.若是是紅黑樹的話就調用紅黑樹的插入法

3.那麼是鏈表結構了,在循環查找以前先判斷鏈表容量大小是否>=TREEIFY_THRESHOLD,是則變爲紅黑樹,而後循環查找Key映射的節點,找到說明已有存在,break;不然在尾部插入節點。

4.若此Key已經存在Value映射了,那就更新值

下面看resize():(初始化tab的時候就有用到,當散列表中元素總量 > 初始大小*裝載因子時,也必須進行resize())相較於JDK1.7,在1.8中resize()方法再也不調用transfer()方法,而是直接將原來transfer()方法中的代碼寫在本身方法體內。

【1】若原來Tab的容量比設定的最大容量都大時,更新threshold爲Integer.MAX_VALUE,不用進行散列

【2】若 最大容量比原來Tab容量的兩倍還大 並且 原來Tab知足大於默認初始化容量大小 那麼新的閾值擴大爲原來的2倍(注意,源碼擴大兩倍或進行2的指數級操做時是使用移位操做符而不是乘號,移位使計算效率高)

【3】若舊容量不是>0, 且原來的閾值就夠大,那直接容許新容量大小跟閾值同樣大

【4】Tab初始化的階段執行過程

【5】其中用到TreeNode的split()方法,看一哈(目的是按照以前順序從新連入lo(w)和hi(gh)列表中)

參考博文

若就散列表存在,根據容量循環整個列表,對於其中非空的數據,複製放在新的table中,判斷:若是隻有一個數據就直接賦值,並肯定存放的位置,當是一個紅黑樹節點時,就按照上面的split()按照以前順序從新連入lo(w)和hi(gh)列表中。(原理與下面進行的鏈表移植原理相同,操做有些許差別)接下來進行鏈表複製,採用  原始位置加原數組長度的方法  計算獲得位置,而非從新計算:【重要!!】

(e.hash & oldCap)

由於【table是2倍擴容,即左移一位】這個與運算,來判斷元素的在數組中的位置是否須要移動,若 =0 則說明其在數組中的位置未發生改變,而新位置 = 原下標位置+原數組長度,即  新的index  =  原來index  +(拼接)  oldCap(原來的數組容量capacity);若 = 1 則說明發生了改變;

(e.hash & (oldCap-1)) 用來獲得其下標位置;【二者大相徑庭!】

接上面的分析:

若是原元素位置沒有發生變化,且low部分沒有元素,將e肯定爲low部分的Head元素,不然,將e加入到low部分的Tail;對於high部分同理。最後完善-->將鏈表的尾節點指向null

小結一下:參考博文

【1】擴容後,新數組中的鏈表順序依然與舊數組中的鏈表順序保持一致!

HashMap底層數組的一些優化: 
【2】數組長度老是2的倍數,擴容則是直接在原有數組長度基礎上乘以2。

有兩個優勢: 
1. 經過與元素的hash值進行與操做,可以快速定位到數組下標 
相對於取模運算,直接進行與操做能提升計算效率。在CPU中,全部的加減乘除都是經過加法實現的,而&(與操做)是CPU直接支持的。 
2. 擴容時簡化計算數組下標的計算量 
由於數組每次擴容都是原來的兩倍,因此每個元素在新數組中的位置要麼是原來的index,要麼index = index + oldCap

接着get():

getNode(hash,key)遍歷尋找並返回節點。

對於remove()方法:

也調用removeNode(xxx),一樣也是遍歷查找,若是找到(key和value都對應),根據其所在位置【鏈表、桶的首位、紅黑樹】進行刪除。

最後關於hashmap和其餘相關的常常在面試中見到的問題進行彙總:

1、HashMap和Hashtable比較:

貼出原文

                                                        HashMap                                        Hashtable

對外接口:                             繼承AbstractMap                               繼承Dictionary

null的鍵值                                        支持                                 不支持,hashCode(0)=0

數據結構                                 鏈表+數組、紅黑樹                              鏈表+數組

默認初始容量                                   16【即桶的數量】                                11

擴容方式                         oldCap<<1【<<一位表示*2】                       oldCap<<1+1

底層數組容量                              2<<n(次數)                                       不要求

確認key數組中的索引                       (n-1)&hash                        (hash & 0x 7FFF-FFFF)%(tab.length)

線程安全                         否【resize中鏈表出現環路->get()】                    是【使用synchronized】

遍歷方式                                        iterator                                     iterator + enumerarion

遍歷數組順序                           index由小到大                                       index由大到小

開發者使用狀況                               很是頻繁                                                     再也不使用

2、其中的hashCode()和equals()方法

       hashCode()和equals()方法。由於在此以前hashCode()屢屢出現,而equals()方法僅僅在獲取值對象的時候纔出現。一些優秀的開發者會指出使用不可變的、聲明做final的對象,而且採用合適的equals()和hashCode()方法的話,將會減小碰撞的發生,提升效率。不可變性使得可以緩存不一樣鍵的hashcode,這將提升整個獲取對象的速度,使用String,Interger這樣的wrapper類做爲鍵是很是好的選擇。

3、若是HashMap的大小超過了負載因子(load factor)定義的容量,怎麼辦?

默認的負載因子大小爲0.75,也就是說,當一個map填滿了75%的bucket時候,和其它集合類(如ArrayList等)同樣,將會建立原來HashMap大小的兩倍的bucket數組,來從新調整map的大小,並將原來的對象放入新的bucket數組中。這個過程叫做rehashing,由於它調用hash方法找到新的bucket位置。

4、HashMap工做原理

HashMap基於hashing原理,咱們經過put()和get()方法儲存和獲取對象。當咱們將鍵值對傳遞給put()方法時,它調用鍵對象的hashCode()方法來計算hashcode,讓後找到bucket位置來儲存值對象。當獲取對象時,經過鍵對象的equals()方法找到正確的鍵值對,而後返回值對象。HashMap使用鏈表來解決碰撞問題,當發生碰撞了,對象將會儲存在鏈表的下一個節點中。 HashMap在每一個鏈表節點中儲存鍵值對對象。

5、當兩個不一樣的鍵對象的hashcode相同時會發生什麼?

它們會儲存在同一個bucket位置【底層數組的位置】的鏈表中。鍵對象的keys.equals()方法用來找到鍵值對。

6、對HashMap中ConcurrentModificationException認識

JDK中源碼註釋:迭代器返回的是這個類的「收集【視圖】方法」是會快速失效的,若是這個鍵值映射在迭代器生成後的任什麼時候間被更改,迭代器就會拋出該異常,除非該修改是經過該迭代器自己的remove()方法。所以在將來的未知時間點及非肯定性的操做,面對多併發修改時,該迭代器就會乾淨利落地失效,而不是隨意冒險。【手動翻譯(太渣了,但意思明白了就行)】

引用博文

原來獲得的keySet和迭代器都是Map中元素的一個「視圖」,而不是「副本」 。問題也就出如今這裏,當一個線程正在迭代Map中的元素時,另外一個線程可能正在修改其中的元素。此時,在迭代元素時就可能會拋出 ConcurrentModificationException異常。

 ConcurrentHashMap提供了和Hashtable以及SynchronizedMap中所不一樣的鎖機制。Hashtable中採用的鎖機制是一次鎖住整個hash表,從而同一時刻只能由一個線程對其進行操做;而ConcurrentHashMap中則是一次鎖住一個桶。     

    ConcurrentHashMap默認將hash表分爲16個桶,諸如get,put,remove等經常使用操做,只鎖當前須要用到的桶。這樣,原來只能一個線程進入,如今卻能同時有16個寫線程執行,併發性能的提高是顯而易見的。 
    在迭代方面,ConcurrentHashMap使用了一種不一樣的迭代方式,即當iterator被建立後集合再發生改變就再也不是拋出ConcurrentModificationException, 取而代之的是  在改變時new新的數據從而不影響原有的數據。 iterator完成後再將頭指針替換爲新的數據。這樣iterator線程可使用原來老的數據。而寫線程也能夠併發的完成改變

7、【今天左8個小時,還沒吃飯,在此先挖個坑,有空就來補充問題】

相關文章
相關標籤/搜索