瞭解HashMap數據結構,超詳細!

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"寫在前面"}]},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"小夥伴兒們,大家好!今天來學習HashMap相關內容,作爲面試必問的知識點,來深入瞭解一波!"}]}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"思維導圖:"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/d1/d1a4c006620f1e619dfa7e6a7cfe6ce5.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":10}}],"text":"學習框架圖"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"1,HashMap集合簡介"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"HashMap基於哈希表的Map接口實現,是以"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}},{"type":"strong"}],"text":"key-value"},{"type":"text","text":"存儲形式存在,即主要用來存放鍵值對。HashMap的實現不是同步的,這意味着它不是線程安全的。它的key、value都可以爲null。此外,HashMap中的映射不是有序的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"JDK1.8之前的HashMap由數組+鏈表組成的,數組是HashMap的主體,鏈表則是主要爲了節解決"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}},{"type":"strong"}],"text":"哈希碰撞"},{"type":"text","text":"(兩個對象調用的hashCode方法計算的哈希碼值一致導致計算的數組索引值相同)而存在的(“拉鍊法”解決衝突)。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"JDK1.8之後在解決哈希衝突時有了較大的變化,當"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}},{"type":"strong"}],"text":"鏈表長度大於閾值"},{"type":"text","text":"(或者紅黑樹的邊界值,默認爲8)並且當前"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}},{"type":"strong"}],"text":"數組的長度大於64"},{"type":"text","text":"時,此時此索引位置上的所有數據改爲使用紅黑樹存儲。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"數組裏面都是key-value的實例,在JDK1.8之前叫做Entry,在JDK1.8之後叫做Node。"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/1c/1c7f5d4f4839b72908faba89c2ab56d9.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"key-value實例"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由於它的key、value都爲null,所以在插入的時候會根據key的hash去計算一個index索引的值。計算索引的方法如下:"}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"/**\n * 根據key求index的過程\n * 1,先用key求出hash值\n */\nstatic final int hash(Object key) {\n int h;\n return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);\n}\n//2,再用公式index = (n - 1) & hash(n是數組長度)\nint hash=hash(key);\nindex=(n-1)&hash;\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏的Hash算法本質上就是三步:取key的hashCode值、高位運算、取模運算。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這樣的話比如說put(\"A\",王炸),插入了key爲\"A\"的元素,這時候通過上述公式計算出插入的位置index,若index爲3則結果如下(即hash(\"A\")=3):"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/7c/7c21871ec7ee8c9f758992d568b6cd3f.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那麼,HashMap中的鏈表又是幹什麼用的呢?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"大家都知道數組的長度是有限的,"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}},{"type":"strong"}],"text":"在有限的長度裏面使用哈希函數計算index的值時,很有可能插入的k值不同,但所產生的hash是相同的"},{"type":"text","text":"(也叫做哈希碰撞),這也就是哈希函數存在一定的概率性。就像上面的K值爲A的元素,如果再次插入一個K值爲a的元素,很有可能所產生的index值也爲3,也就是即hash(\"a\")=3;那這就形成了鏈表,這種解決哈希碰撞的方法也叫做拉鍊法。"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/06/06c43bf7aa4b497d858503e7c76c4ba7.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"當這個鏈表長度大於閾值8並且數組長度大於64則進行將鏈表變爲紅黑樹。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"補充:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"將鏈表轉換成紅黑樹前會判斷,"},{"type":"text","marks":[{"type":"strong"}],"text":"如果閾值大於8,但是數組長度小64,此時並不會將鏈表變爲紅黑樹。而是選擇進行數組擴容。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這樣做的目的是因爲數組比較小,儘量避開紅黑樹結構,這種情況下變爲紅黑樹結構,反而會降低效率,因爲紅黑樹需要進行左旋,右旋,變色這些操作來保持平衡。同事數組長度小於64時,搜索時間相對快一些。所以綜上所述爲了提高性能和減少搜索時間,底層在閾值大於8並且數組長度大於64時,鏈表才轉換爲紅黑樹。具體可以參考treeifyBin方法。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當然雖然增了紅黑樹作爲底層數據結構,結構變得複雜了,但是閾值大於8並且數組長度大於64時,鏈表轉換爲紅黑樹時,效率也變得更高效。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"特點:"}]},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"存取無序的"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"鍵和值位置都可以是null,但是鍵位置只能是一個null"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"鍵位置是唯一的,底層的數據結構控制鍵的"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"jdk1.8前數據結構是:鏈表 + 數組  jdk1.8之後是 :鏈表 + 數組  + 紅黑樹"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":5,"align":null,"origin":null},"content":[{"type":"text","text":"閾值(邊界值) > 8 並且數組長度大於64,纔將鏈表轉換爲紅黑樹,變爲紅黑樹的目的是爲了高效的查詢。"}]}]}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"2,HsahMap底層數據結構"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2.1,HashMap存儲數據的過程"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/33/3372df90aec7531aa0993b6c5fb5ce3e.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"每一個Node結點都包含"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}},{"type":"strong"}],"text":"鍵值對的key,value"},{"type":"text","text":"還有計算出來的"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}},{"type":"strong"}],"text":"hash值"},{"type":"text","text":",還保存着下一個"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}}],"text":" "},{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}},{"type":"strong"}],"text":"Node 的引用 next"},{"type":"text","text":"(如果沒有下一個 Node,next = null),來看看Node的源碼:"}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"static class Node implements Map.Entry {\n final int hash;\n final K key;\n V value;\n Node next;\n ...\n }"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"HashMap存儲數據需要用到put()方法,關於這些方法的詳解,我們下節再說,這裏簡要說一下;"}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"public static void main(String[] args) {\n HashMap hmap=new HashMap<>();\n hmap.put(\"斑\",55);\n hmap.put(\"鏡\",63);\n hmap.put(\"帶土\",25);\n hmap.put(\"鼬\",9);\n hmap.put(\"佐助\",43);\n hmap.put(\"斑\",88);\n System.out.println(hmap);\n }"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當創建HashMap集合對象的時候,在jdk1.8之前,構造方法中會創建很多"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}},{"type":"strong"}],"text":"長度是16的Entry[] table"},{"type":"text","text":"用來存儲鍵值對數據的。在jdk1.8之後不是在HashMap的構造方法底層創建數組了,是在"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}},{"type":"strong"}],"text":"第一次調用put方法"},{"type":"text","text":"時創建的數組,"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}},{"type":"strong"}],"text":"Node[] table"},{"type":"text","text":"用來存儲鍵值對數據的。"}]},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"比方說我們向哈希表中存儲\"斑\"-55的數據,根據K值(\"斑\")調用String類中重寫之後的hashCode()方法計算出值(數量級很大),然後結合數組長度採用取餘((n-1)&hash)操作或者其他操作方法來計算出向Node數組中存儲數據的空間的索引值。如果計算出來的索引空間沒有數據,則直接將\"斑\"-55數據存儲到數組中。跟上面的\"A-王炸\"數據差不多。"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們回到上方的數組圖,如果此時再插入\"A-蘑菇\"元素,那麼首先根據Key值(\"A\")調用hashCode()方法結合數組長度計算出索引肯定也是3,此時比較後存儲的\"A-蘑菇\"和已經存在的數據\"A-王炸\"的hash值是否相等,如果hash相等,此時發生hash碰撞。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那麼底層會調用\"A\"所屬"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}},{"type":"strong"}],"text":"類String中的equals方法"},{"type":"text","text":"比較兩個key內容是否相等,若相等,則後添加的數據直接覆蓋已經存在的Value,也就是\"蘑菇\"直接覆蓋\"王炸\";若不相等,繼續向下和其他數據的key進行比較,如果都不相等,則規劃出一個節點存儲數據。"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/7d/7dce818b614372db22deffaffcba10c2.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"兩個結點key值比較,是否覆蓋"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2.2,哈希碰撞相關的問題"}]},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"哈希表底層採用何種算法計算hash值?還有哪些算法可以計算出hash值?"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"底層是採用key的hashCode方法的值結合數組長度進行"},{"type":"text","marks":[{"type":"strong"}],"text":"無符號右移(>>>)"},{"type":"text","text":"、按位異或(^)、按位與(&)計算出索引的"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"還可以採用:平方取中法,取餘數,僞隨機數法。這三種效率都比較低。而無符號右移16位異或運算效率是最高的。"}]},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當兩個對象的hashCode相等時會怎麼樣?"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"會產生哈希碰撞,若"},{"type":"text","marks":[{"type":"strong"}],"text":"key值內容相同則替換舊的value"},{"type":"text","text":".否則連接到鏈表後面,"},{"type":"text","marks":[{"type":"strong"}],"text":"鏈表長度超過閾值8"},{"type":"text","text":"就轉換爲紅黑樹存儲。"}]},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"何時發生哈希碰撞和什麼是哈希碰撞,如何解決哈希碰撞?"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"只要"},{"type":"text","marks":[{"type":"strong"}],"text":"兩個元素的key計算的哈希值相同"},{"type":"text","text":"就會發生哈希碰撞。jdk8前使用鏈表解決哈希碰撞。jdk8之後使用鏈表+紅黑樹解決哈希碰撞。"}]},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果兩個鍵的hashcode相同,如何存儲鍵值對?"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"hashcode相同,通過equals比較內容是否相同。"},{"type":"text","marks":[{"type":"strong"}],"text":"相同:則新的value覆蓋之前的value 不相同:則將新的鍵值對添加到哈希表中"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2.3,紅黑樹結構"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當位於一個鏈表中的元素較多,即hash值相等但是內容不相等的元素較多時,通過key值依次查找的效率較低。而jdk1.8中,哈希表存儲採用數組+鏈表+紅黑樹實現,當鏈表長度(閥值)超過 8 時且當前數組的長度 > 64時,將鏈表轉換爲紅黑樹,這樣大大減少了查找時間。jdk8在哈希表中"},{"type":"text","marks":[{"type":"strong"}],"text":"引入紅黑樹的原因只是爲了查找效率更高。"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/2d/2d6bf6020091655bf7dade250e72cbc8.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"紅黑樹結構"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"JDK 1.8 以前 HashMap 的實現是 數組+鏈表,即使哈希函數取得再好,也很難達到元素百分百均勻分佈。當 HashMap 中有大量的元素都存放到同一個桶中時,這個桶下有一條長長的鏈表,這個時候 HashMap 就相當於一個單鏈表,假如單鏈表有 n 個元素,遍歷的時間複雜度就是 O(n),完全失去了它的優勢。針對這種情況,JDK 1.8 中引入了 紅黑樹("},{"type":"text","marks":[{"type":"strong"}],"text":"查找時間複雜度爲 O(logn)"},{"type":"text","text":")來優化這個問題。當鏈表長度很小的時候,即使遍歷,速度也非常快,但是當鏈表長度不斷變長,肯定會對查詢性能有一定的影響,所以才需要轉成樹。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2.4,存儲流程圖"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"HashMap存放數據是用的put方法,put 方法內部調用的是 putVal() 方法,所以對 put 方法的分析也是對 putVal 方法的分析,整個過程比較複雜,流程圖如下:"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/e4/e40955237c51d77cbddd5d04b53a66f6.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"來看看put()源碼:"}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"public V put(K key, V value) {\n //對key的hashCode()做hash,調用的是putVal方法\n return putVal(hash(key), key, value, false, true);\n }\n\n final V putVal(int hash, K key, V value, boolean onlyIfAbsent,\n boolean evict) {\n Node[] tab; Node p; int n, i;\n /*\n 1,tab爲空則開始創建,\n 2,(tab = table) == null 表示將空的table賦值給tab,然後判斷tab是否等於null,第一次肯定是null\n 3,(n = tab.length) == 0 表示沒有爲table分配內存\n 4,tab爲空,執行代碼 n = (tab = resize()).length; 進行擴容。並將初始化好的數組長度賦值給n.\n 5,執行完n = (tab = resize()).length,數組tab每個空間都是null\n */\n \n if ((tab = table) == null || (n = tab.length) == 0)\n //調用resize()方法進行擴容\n n = (tab = resize()).length;\n /*\n 1,i = (n - 1) & hash 表示計算數組的索引賦值給i,即確定元素存放在哪個桶中\n 2,p = tab[i = (n - 1) & hash]表示獲取計算出的位置的數據賦值給節點p\n 3,(p = tab[i = (n - 1) & hash]) == null 判斷節點位置是否等於null,\n 如果爲null,則執行代碼:tab[i] = newNode(hash, key, value, null);根據鍵值對創建新的節點放入該位置的桶中\n 小結:如果當前桶沒有哈希碰撞衝突,則直接把鍵值對插入空間位置\n */ \n if ((p = tab[i = (n - 1) & hash]) == null)\n //節點位置爲null,則直接進行插入操作\n tab[i] = newNode(hash, key, value, null);\n //節點位置不爲null,表示這個位置已經有值了,於是需要進行比較hash值是否相等\n else {\n Node e; K k;\n /*\n 比較桶中第一個元素(數組中的結點)的hash值和key是否相等\n 1,p.hash == hash 中的p.hash表示原來存在數據的hash值 hash表示後添加數據的hash值 比較兩個hash值是否相等\n 2,(k = p.key) == key :p.key獲取原來數據的key賦值給k key表示後添加數據的key 比較兩個key的地址值是否相等\n 3,key != null && key.equals(k):能夠執行到這裏說明兩個key的地址值不相等,那麼先判斷後添加的key是否等於null,如果不等於null再調用equals方法判斷兩個key的內容是否相等\n */\n if (p.hash == hash &&\n ((k = p.key) == key || (key != null && key.equals(k))))\n /*\n 說明:兩個元素哈希值相等(哈希碰撞),並且key的值也相等\n 將舊的元素整體對象賦值給e,用e來記錄\n */ \n e = p;\n // hash值不相等或者key不相等;判斷p是否爲紅黑樹結點\n else if (p instanceof TreeNode)\n // 是紅黑樹,調用樹的插入方法\n e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);\n // 說明是鏈表節點,這時進行插入操作\n else {\n /*\n 1,如果是鏈表的話需要遍歷到最後節點然後插入\n 2,採用循環遍歷的方式,判斷鏈表中是否有重複的key\n */\n for (int binCount = 0; ; ++binCount) {\n /*\n 1)e = p.next 獲取p的下一個元素賦值給e\n 2)(e = p.next) == null 判斷p.next是否等於null,等於null,說明p沒有下一個元 素,那麼此時到達了鏈表的尾部,還沒有找到重複的key,則說明HashMap沒有包含該鍵\n 將該鍵值對插入鏈表中\n */\n if ((e = p.next) == null) {\n p.next = newNode(hash, key, value, null);\n //插入後發現鏈表長度大於8,轉換成紅黑樹結構\n if (binCount >= TREEIFY_THRESHOLD - 1) \n //轉換爲紅黑樹\n treeifyBin(tab, hash);\n break;\n }\n //key值以及存在直接覆蓋value\n if (e.hash == hash &&\n ((k = e.key) == key || (key != null && key.equals(k))))\n break;\n p = e;\n }\n }\n //若結點爲null,則不進行插入操作\n if (e != null) { \n V oldValue = e.value;\n if (!onlyIfAbsent || oldValue == null)\n e.value = value;\n afterNodeAccess(e);\n return oldValue;\n }\n }\n //修改記錄次數\n ++modCount;\n // 判斷實際大小是否大於threshold閾值,如果超過則擴容\n if (++size > threshold)\n resize();\n // 插入後回調\n afterNodeInsertion(evict);\n return null;\n }"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"小結:"}]},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"根據哈希表中元素個數確定是"},{"type":"text","marks":[{"type":"strong"}],"text":"擴容還是樹形化"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"如果是樹形化遍歷桶中的元素,創建相同個數的樹形節點,複製內容,建立起聯繫"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"然後讓桶中的第一個元素指向新創建的樹根節點,替換桶的鏈表內容爲樹形化內容"}]}]}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"3,HashMap的擴容機制"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們知道,數組的容量是有限的,多次插入數據的話,到達一定數量就會進行擴容;先來看兩個問題"}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"什麼時候需要擴容?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當HashMap中的"},{"type":"text","marks":[{"type":"strong"}],"text":"元素個數超過數組長度loadFactor(負載因子)"},{"type":"text","text":"時,就會進行數組擴容,loadFactor的默認值是0.75,這是一個折中的取值。也就是說,默認情況下,數組大小爲16,那麼當HashMap中的元素個數超過16×0.75=12(這個值就是閾值)的時候,就把數組的大小擴展爲2×16=32,即擴大一倍,然後"},{"type":"text","marks":[{"type":"strong"}],"text":"重新計算每個元素在數組中的位置"},{"type":"text","text":",而這是一個非常耗性能的操作,所以如果我們已經預知HashMap中元素的個數,那麼預知元素的個數能夠有效的提高HashMap的性能。"}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"怎麼進行擴容的?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"HashMap在進行擴容時使用 resize() 方法,計算 table 數組的新容量和 Node 在新數組中的新位置,將舊數組中的值複製到新數組中,從而實現自動擴容。因爲每次擴容都是翻倍,與原來計算的 (n-1)&hash的結果相比,只是多了一個bit位,所以節點要麼就在原來的位置,要麼就被分配到\""},{"type":"text","marks":[{"type":"strong"}],"text":"原位置+舊容量"},{"type":"text","text":"\"這個位置。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因此,我們在擴充HashMap的時候,不需要重新計算hash,只需要看看原來hash值新增的那個bit是1還是0就可以了,是0的話索引沒變,是1的話索引變成“原索引+oldCap("},{"type":"text","marks":[{"type":"strong"}],"text":"原位置+舊容量"},{"type":"text","text":")”。這裏不再詳細贅述,可以看看下圖爲16擴充爲32的resize示意圖:"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/fc/fc1b19df3ec331e181c92fa047b08dff.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"hashmap擴容"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"4,HashMap數組長度爲什麼是2的次冪"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們先看看它的成員變量:"}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"序列化版本號"}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"private static final long serialVersionUID = 362498820763181265L;"}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"集合的初始化容量initCapacity"}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"//默認的初始容量是16 -- 1<<4相當於1*2的4次方---1*16\nstatic final int DEFAULT_INITIAL_CAPACITY = 1 << 4; "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"初始化容量默認是16,容量過大,遍歷時會減慢速度,效率低;容量過小,那麼擴容的次數變多,非常耗費性能。"}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"負載因子"}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"/**\n * The load factor used when none specified in constructor.\n */\n static final float DEFAULT_LOAD_FACTOR = 0.75f;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"初始默認值爲0.75,若過大,會導致哈希衝突的可能性更大;若過小,擴容的次數也會提高。"}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"爲什麼必須是2的n次冪?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當向HashMap中添加一個元素的時候,需要根據key的hash值,去確定其在數組中的具體位置。HashMap爲了提高存取效率,要儘量較少碰撞,就是要儘量把數據分配均勻,每個鏈表長度大致相同,這個實現就在把數據存到哪個鏈表中的算法。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個算法實際就是取模,hash%length,計算機中直接求餘效率不如位移運算。所以源碼中做了優化,使用 "},{"type":"codeinline","content":[{"type":"text","text":"hash&(length-1)"}]},{"type":"text","text":",而實際上"},{"type":"codeinline","content":[{"type":"text","text":"hash%length"}]},{"type":"text","text":"等於"},{"type":"codeinline","content":[{"type":"text","text":"hash&(length-1)"}]},{"type":"text","text":"的前提是length是2的n次冪。"}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"如果輸入值不是2的冪會怎麼樣?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果數組長度不是2的n次冪,計算出的索引特別容易相同,及其容易發生hash碰撞,導致其餘數組空間很大程度上並沒有存儲數據,鏈表或者紅黑樹過長,效率降低。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"小結:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1,當根據key的hash確定其在數組的位置時,如果n爲2的冪次方"},{"type":"text","marks":[{"type":"strong"}],"text":",可以保證數據的均勻插入"},{"type":"text","text":",如果n不是2的冪次方,可能數組的一些位置永遠不會插入數據,"},{"type":"text","marks":[{"type":"strong"}],"text":"浪費數組的空間,加大hash衝突。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2,一般可能會想通過 % 求餘來確定位置,這樣也可以,只不過性能不如 & 運算。而且當n是2的冪次方時:hash & (length - 1) == hash % length"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3,因此,HashMap 容量爲2次冪的原因,就是"},{"type":"text","marks":[{"type":"strong"}],"text":"爲了數據的的均勻分佈,減少hash衝突,"},{"type":"text","text":"畢竟hash衝突越大,代表數組中一個鏈的長度越大,這樣的話會降低hashmap的性能"}]},{"type":"horizontalrule"},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"微信搜索公衆號《"},{"type":"text","marks":[{"type":"size","attrs":{"size":12}},{"type":"color","attrs":{"color":"#F5222D","name":"red"}}],"text":"程序員的時光"},{"type":"text","text":"》"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#000000","name":"black"}}],"text":"好了,今天就先分享到這裏了,下期繼續給大家帶來HashMap面試內容!"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#000000","name":"black"}}],"text":"更多幹貨、優質文章,歡迎關注我的原創技術公衆號~"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章