1. 程式人生 > >HashMap:從原始碼分析到面試題

HashMap:從原始碼分析到面試題

## 1 HashMap簡介 HashMap是實現map介面的一個重要實現類,在我們無論是日常還是面試,以及工作中都是一個經常用到角色。它的結構如下: ![](https://upload-images.jianshu.io/upload_images/22691891-4d7db23aeb6760c0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 它的底層是用我們的雜湊表和紅黑樹組成的。所以我們在學習HashMap底層原理的時候,需要有這兩種資料結構的知識做鋪墊,才能有更好的理解! ### 1.1 雜湊表 散列表是由我們的陣列和連結串列組成的,集成了兩種資料結構的優點,我們先簡單介紹一下這兩種資料結構。 **陣列:**陣列儲存區間是連續的,佔用記憶體嚴重,故空間複雜度很大,但陣列的二分查詢時間複雜度很小,為 o(1),陣列的特點:**查詢速度快、插入和刪除效率低** **連結串列:**連結串列儲存區間離散,佔用記憶體比較寬鬆,故空間複雜度很小,但時間複雜度很大,為 o(n),連結串列的特點:**查詢速度慢、插入和刪除效率高** **雜湊表:**雜湊表**為每個物件計算出一個整數,稱為雜湊碼**。**根據**這些計算出來的**整數(雜湊碼)儲存在對應的位置上**!如果遇到了雜湊衝突,也就是同一個坑遇到了被佔用的情況下,那麼我們就會以連結串列的形式新增在後面。 ![](https://upload-images.jianshu.io/upload_images/22691891-7b19d9efc5f51a76.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) ### 1.2 紅黑樹 關於紅黑樹的知識點比較多,如果過多介紹紅黑樹的話,那麼HashMap就不好介紹了。這裡給上一個連線,一篇關於紅黑樹非常好的文章。[點選這裡](https://www.jianshu.com/p/e136ec79235c) ## 2 原始碼解析 好了,開始解析我們的原始碼,通過解析原始碼更好的瞭解HashMap後,對那麼常見的面試題也可以更加的吃透! ### 2.1 基本屬性 首先就是介紹我們的HashMap的基本屬性,對基本屬性介紹完之後,對後面方法裡使用時才不會迷惑 1、我們的預設的初始化的hashmap的容量,如果沒有指定的話,就是我們的預設,1<<4就是16。 ```java static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 ``` 2、我們的hashmap最大容量,2的30次方。 ```java static final int MAXIMUM_CAPACITY = 1 << 30; ``` 3、預設的裝載因子,0.75。有什麼用呢?比如我們的容量現在是16,16*0.75=12,也就是說,當我們的實際容量到了12的時候,那麼就會觸發擴容機制,進行擴容! ```java static final float DEFAULT_LOAD_FACTOR = 0.75f; ``` 4、我們知道雜湊表是由陣列和連結串列組成的,每一個位置都可以說是一個雜湊桶。我們的雜湊桶預設是連結串列,但是在JDK1.8之後我們的雜湊桶中當有TREEIFY_THRESHOLD個節點的時候,也就是下面預設的8,我們桶中的連結串列會被轉換為紅黑樹的結構。 ```java static final int TREEIFY_THRESHOLD = 8; ``` 5、與上面相同,不過不同的是,會將紅黑樹轉換成連結串列。 ```java static final int UNTREEIFY_THRESHOLD = 6; ``` 6、當雜湊桶的結構轉換成樹之前,還會有一次判斷,只有鍵值對大於64才會轉換!也就是我們下面定義的最小容量,這是為了避免雜湊表建立初期多個鍵值對恰巧都在一個雜湊桶上面,而導致了沒必要的轉換。 ```java static final int MIN_TREEIFY_CAPACITY = 64; ``` 7、內部結構靜態內部類 ![](https://upload-images.jianshu.io/upload_images/22691891-9a6785dbb79389fd.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 8、其他成員變數 ![](https://img2020.cnblogs.com/blog/1993240/202008/1993240-20200804221738320-2052919425.png) #### 2.1.1 思考 這裡同時引發了我們一些思考?為什麼要將轉換成樹形結構的閾值設定為8呢?為什麼不將轉換成連結串列結構的閾值也設定為8呢?這裡我們在最後面試題分析的時候統一進行回答! ### 2.2 構造方法 hashmap的構造方法有四個,不過我們重點介紹其中的一個,因為這一個理解了,其他的也不成問題。 ```java //initialCapacity:初始大小 //loadFactor:裝載因子 public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; this.threshold = tableSizeFor(initialCapacity); } ``` 總結了構造方法進行的操作: 1. 首先是邊界處理,如果初始大小小於0,拋異常。如果大於最大,那也只能賦予我們預設的最大值!如果裝載因子小於0或者不是數字的話,拋異常! 2. 然後就是進行我們的賦值,裝載因子賦值,還有就是呼叫我們的`tableSizeFor`來返回一個大於等於initialCapacity的2次冪。 ```java static final int tableSizeFor(int cap) { int n = cap - 1; n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; } ``` 關於為什麼做了位運算後可以返回大於等於它的二次冪,可以看一下這篇博文![點選跳轉](https://blog.csdn.net/fan2012huan/article/details/51097331) 這裡的threshold也就是我們的閾值,當達到了這個閾值的時候我們會進行擴容!但是這裡可能也會覺得疑惑,閾值不是容量*裝載因子嗎?不應該寫成下面這樣子嗎? ```java this.threshold = tableSizeFor(initialCapacity) * this.loadFactor; ``` 注意,在構造方法中,並沒有對table這個成員變數進行初始化,table的初始化被推遲到了put方法中,在put方法中會用到resize()方法,然後對threshold重新計算。後面我們對方法分析時會談到。 ### 2.3 核心方法 關於hashmap和核心方法和考點,其實都集中在put方法和resize()方法,這也會是我們下面重點要介紹到的。 #### 2.3.1 put方法 我們首先來看put方法 ```java public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } ``` 呼叫了我們的putval方法,參入了一個以key計算的雜湊值,key,value,還有兩個其他引數。在看putVal方法之前先來看一下hash方法,看看它是如何計算雜湊值。 ```java static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } ``` 這是一個三目運算子,如果key不為null的話,返回我們的key的雜湊值(低十六位)同時與高16位的異或運算。這一步的操作意義何為呢?我們先臨時跳到putVal方法裡面可以看到有這麼一步操作 ![](https://upload-images.jianshu.io/upload_images/22691891-42614d824031ce0f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 它將我們計算出來的雜湊值,與我們的雜湊表長度-1(為了獲得)進行`&`運算,這是為了獲取我的table下標。至於為什麼-1呢?因為我們的長度都是2的整數次冪,轉換成2進位制也就是1000000....這種的形式,為了更好的隨機,所有我們進行了-1操作,也就是變成11111111這種。因為`&`操作是都為1的時候才會為1,所以我的的1多的時候隨機性才會更大,畢竟一個1能幹過那麼多的1嗎?這是減少雜湊衝突的第一步操作。舉個例子說明一下: ``` 比如我們的長度轉換為2進製為 1000 0000 ,進行-1操作後就是 0111 1111 而這個時候我們原來的二進位制數 1000 0000 & 0101 1011 = 0000 0000 與任何最高位不為1的數進行&運算,都會變成0,也就讓我們的雜湊衝突變大了! 而我們-1操作後 0111 1111 & 0101 1011 = 0101 1011 可以看出來,這樣比原來的減少了很多的雜湊衝突。 同時這也是為什麼我們要讓雜湊的容量大小一定要為2的整數次冪 ``` 好了,我們要回答一下再上面那個問題了,為什麼要返回低16位與高16位的異或作為key的最終hash值呢?同樣舉個例子演示一下這個流程: 假設length為8,HashMap的預設初始容量為16; length = 8 ,(length-1) = 7 , 轉換二進位制為111; 假設一個key的 hashcode = 78897121 ,轉換二進位制:100101100111101111111100001,與(length-1)& 運算如下 ```java 0000 0100 1011 0011 1101 1111 1110 0001 &運算 0000 0000 0000 0000 0000 0000 0000 0111 = 0000 0000 0000 0000 0000 0000 0000 0001 (就是十進位制1,所以下標為1) ``` **上述運算實質是:001 與 111 & 運算。也就是雜湊值的低三位與length與運算。如果讓雜湊值的低三位更加隨機,那麼&結果就更加隨機,就更能減少我們的雜湊衝突了。如何讓雜湊值的低三位更加隨機,那麼就是讓其與高位異或,所以我們才在返回的時候與高位異或了再返回。低位與高位異或的過程舉個例子如下:** ![image.png](https://upload-images.jianshu.io/upload_images/22691891-6098de78357eaa64.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 然後總結一下在與我們與雜湊值進行運算的時候有這麼一個規律: - **當length=8時 下標運算結果取決於雜湊值的低三位** - **當length=16時 下標運算結果取決於雜湊值的低四位** - **當length=32時 下標運算結果取決於雜湊值的低五位** - **當length=2的N次方, 下標運算結果取決於雜湊值的低N位。** 好了,我們繼續回到我們的putVal方法。下面我直接在註釋裡面進行分析 ```java final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node[] tab; Node p; int n, i; //當我們的table為空的時候呼叫resize()進行擴容初始化 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 沒有發生碰撞,初始化我們的第一個節點 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); //發生碰撞 else { Node e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) //hashcode和key相等,記錄下原先的值 e = p; //如果這個時候我們的雜湊桶已經是紅黑樹結構,那麼呼叫樹的插入函式 else if (p instanceof TreeNode) e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value); else { //連結串列結構,同時我們的hashcode不相等 //找到與key相等的節點,更新value,退出迴圈 //如果沒有找到與key相等的節點,在連結串列尾部插入,如果插入後節點數量大於 //我們變成紅黑樹的閾值,那麼進行轉換成紅黑樹 for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); //達到臨界值轉換成紅黑樹 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; //空實現,為LinkedHashMap預留 afterNodeAccess(e); return oldValue; } } ++modCount; //鍵值對達到閾值,進行擴容 if (++size > threshold) resize(); //空實現,為LinkedHashMap預留 afterNodeInsertion(evict); return null; } ``` #### 2.3.2 resize()方法 我們在上面不管是原始碼分析還是在哪分析,都說到了我們的resize()方法,下面我們將正式開始講到 ```java final Node[] resize() { //原table陣列賦值 Node[] oldTab = table; //如果原陣列為null,那麼原陣列長度為0 int oldCap = (oldTab == null) ? 0 : oldTab.length; //賦值閾值 int oldThr = threshold; //newCap 新陣列長度 //newThr 下次擴容的閾值 int newCap, newThr = 0; // 1. 如果原陣列長度大於0 if (oldCap > 0) { //如果大於最大長度1 << 30 = 1073741824,那麼閾值賦值為Integer.MAX_VALUE後直接返回 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } // 2. 如果原陣列長度的2倍小於最大長度,並且原陣列長度大於預設長度16,那麼新閾值為原閾值的2倍 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } // 3. 如果原陣列長度等於0,但原閾值大於0,那麼新的陣列長度賦值為原閾值大小 else if (oldThr >
0) // initial capacity was placed in threshold newCap = oldThr; else { // zero initial threshold signifies using defaults // 4. 如果原陣列長度為0,閾值為0,那麼新陣列長度,新閾值都初始化為預設值 newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // 5.如果新的閾值等於0 if (newThr == 0) { //計算臨時閾值 float ft = (float)newCap * loadFactor; //新陣列長度小於最大長度,臨時閾值也小於最大長度,新閾值為臨時閾值,否則是Integer.MAX_VALUE newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } //計算出來的新閾值賦值給物件的閾值 threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) //用新計算的陣列長度新建一個Node陣列,並賦值給物件的table Node[] newTab = (Node[])new Node[newCap]; table = newTab; //後面是copy陣列和連結串列資料邏輯 if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node 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)e).split(this, newTab, j, oldCap); else { // preserve order Node loHead = null, loTail = null; Node hiHead = null, hiTail = null; Node 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; } ``` 這個時候我們以最初的三種構造方法來模擬一下流程。上面每一個擴容情況都標註了記號 ```java //① Map map = new HashMap<>(); map.put("1", "1"); //② Map map1 = new HashMap<>(2); map1.put("2", "2"); //③ Map map2 = new HashMap<>(2, 0.5f); map2.put("3", "3"); ``` - ① 沒有設定initialCapacity,也沒有設定負載因子,第一次put的時候會觸發擴容。第一次的時候,陣列長度為預設值16,閾值為16*0.75=12,走的`程式碼4`邏輯,等到陣列長度超過閾值12後,觸發第二次擴容,此時table陣列,和threshold都不為0,即oldTab、oldCap、oldThr都不為0,先走`程式碼1`,如果oldCap長度的2倍沒有超過最大容量,並且oldCap 長度大於等於 預設容量16,那麼下次擴容的閾值 變為oldThr大小的兩倍即 12* 2 = 24,newThr = 24,newCap=32 - ② 設定了initialCapacity,沒有設定負載因子,此時hashMap使用預設負載因子0.75,本例項設定的初始容量為2,通過計算閾值為2,第一次put的時候由於還沒初始化table陣列,因此觸發第一次擴容。此時oldCap為0,oldThr為2,走`程式碼3`,確定這次擴容的新陣列大小為2,此時還沒有確定newThr 下次擴容的大小,於是進入`程式碼5` 確定newThr為 2 *0.75 = 1.5 取整 1 ,及下次擴容閾值為1。當陣列已有元素大於閾值及1時,觸發第二次擴容,此時oldCap為1,oldThr為1,走`程式碼1`newCap = oldCap << 1 結果為 4 小於最大容量, 但oldCap 小於hashMap預設大小16,結果為false,跳出判斷,此時由於newThr等於0,進入`程式碼5`,確定newThr為 4* 0.75 = 3,下次擴容閾值為3 - ③ 設定了initialCapacity=2,負載因子為0.5,通過tableSizeFor計算閾值為2,第一次put的時候,進行擴容,此時oldCap為2,oldThr為2,進入`程式碼1`,同例項②,newCap = oldCap << 1 結果為 4 小於最大容量, 但oldCap 小於hashMap預設大小16,結果為false,跳出判斷,進入`程式碼5`,確定newThr為 4 * 0.5 = 2,下次擴容閾值為2 #### 2.3.3 get()方法 ```java public V get(Object key) { Node e; return (e = getNode(hash(key), key)) == null ? null : e.value; } ``` 獲取了我們的key的hashcode然後作為引數傳入getNode方法中! ```java final Node getNode(int hash, Object key) { Node[] tab; Node first, e; int n; K k; //如果計算出來的雜湊值是在雜湊表上 if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { //如果剛好在雜湊桶的第一個節點上,返回 if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; //如果不在第一個節點,遍歷節點 if ((e = first.next) != null) { //如果這個時候我們的雜湊桶已經是樹形結構了,呼叫樹形查詢 if (first instanceof TreeNode) return ((TreeNode)first).getTreeNode(hash, key); //對我們的連結串列進行遍歷查詢元素 do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } //如果沒有找到的話,返回null return null; } ``` ### 2.3.4 remove方法 ```java public V remove(Object key) { Node e; return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value; } ``` 首先是計算出我們的hash,然後呼叫removeNode方法來移除 ```java final Node removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) { Node[] tab; Node p; int n, index; //我們的雜湊桶不為空,同時要對映的雜湊值也在 if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) { Node node = null, e; K k; V v; //剛好我們的雜湊桶首位就是要刪除的,記錄下來 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) node = p; //如果不是,進行遍歷查詢 else if ((e = p.next) != null) { //如果是紅黑樹結構的話,呼叫樹的查詢方法 if (p instanceof TreeNode) node = ((TreeNode)p).getTreeNode(hash, key); else { //對連結串列進行查詢key do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { node = e; break; } p = e; } while ((e = e.next) != null); } } //找到了之後就去刪除,分紅黑樹,桶的首位,連結串列中, if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) { //紅黑樹,呼叫樹刪除節點的方法 if (node instanceof TreeNode) ((TreeNode)node).removeTreeNode(this, tab, movable); //在桶首位,直接將下一個節點賦值給首位 else if (node == p) tab[index] = node.next; //在連結串列中,將下一個結點賦值給前一個節點的下一個節點,刪除自我 else p.next = node.next; ++modCount; --size; afterNodeRemoval(node); return node; } } return null; } ``` ## 3 總結 1. 擴容是一個特別耗效能的操作,所以當我們在使用 HashMap,正確估算 map 的大小,初始化的時候給一個大致的數值,避免 map 進行頻繁的擴容。 2. 負載因子 loadFactor 是可以修改的,也可以大於1,但是建議不要輕易修改,除非情況特殊。 3. HashMap 是非執行緒安全的,不要在併發的情況下使用 HashMap,建議使用 ConcurrentHashMap! ## 4 面試題分析 關於HashMap的原始碼就分析這些,因為這些足夠我們去了解它的一些基本特性和常見面試足夠用了。下面我收集了一些面試題和我們上面的留下的思考題進行分析! **1、為什麼要將轉換成樹形結構的閾值設定為8呢?為什麼不將轉換成連結串列結構的閾值也設定為8呢?** 1. 當初始閾值為8時,連結串列的長度達到8的概率變的很小,如果再大概率減小的並不明顯 2. 樹結構查詢的時間複雜度是O(log(n)),而連結串列的時間複雜度是O(n),當閾值為8時,long8 = 3,相比連結串列更快,但樹結構比連結串列佔用的空間更多,所以這是一種時間和空間的平衡 至於為什麼不將轉換連結串列的閾值也設定為8,是因為如果兩個值太接近的話,就會造成頻繁的轉換,導致我們的時間複雜度變高。而在6是經過計算後最合適的數值 **2、HashMap 為什麼不用平衡樹,而用紅黑樹?** 這一題應該歸類與資料結構了,不過這裡同樣給出分析 - 紅黑樹也是一種平衡樹,但不是嚴格平衡,平衡樹是左右子樹高度差不超過1,紅黑樹可以是2倍 - 紅黑樹在插入、刪除的時候旋轉的概率比平衡樹低很多,效率比平衡樹高 查詢時間複雜度都維持在O(logN),具體的還望檢視紅黑樹的特性,上面最開始也給了一篇關於紅黑樹的介紹。 **3、HashMap在併發下會產生什麼問題?有什麼替代方案?** HashMap併發下產生問題:由於在發生hash衝突,插入連結串列的時候,多執行緒會造成環鏈,再get的時候變成死迴圈,Map.size()不準確,資料丟失。 關於為什麼會造成環鏈的話,[可以看這裡!](http://www.imooc.com/article/details/id/292265) 替代方案: - HashTable: 通過synchronized來修飾,效率低,多執行緒put的時候,只能有一個執行緒成功,其他執行緒都處於阻塞狀態 - ConcurrentHashMap: 1.7 採用鎖分段技術提高併發訪問率 1.8 資料依舊是分段儲存,但鎖採用了synchronized **4、HashMap中的key可以是任何物件或資料型別嗎?** - 可以是null,但不能是可變物件,如果是可變物件,物件中的屬性改變,則物件的HashCode也相應改變,導致下次無法查詢到已存在Map中的資料 - 如果要可變物件當著鍵,必須保證其HashCode在成員屬性改變的時候保持不變 **5、為什麼不直接將key作為雜湊值而是與高16位做異或運算?** 這個我們在上面說過了,還用圖和樣例解釋,是為了更好的隨機性,解決雜湊碰撞。 **6、關於更多的面試題** 這裡提供了一篇關於面試題挺多的博文,通過閱讀原始碼,裡面大部分的面試題都可以解答了! [點選這裡看面試題](https://www.cnblogs.com/zengcongcong/p/11295349.html) ## 5 HashMap與HashTable有什麼不同? 因為HashTable和HashMap很是類似,就跟我們的Vector與ArrayList的關係一樣。提供了執行緒安全的解決方案,所有我們在這裡通過區別,就相當與對HashTable進行了原始碼分析! > 從儲存結構和實現來講基本上都是相同的。 > > 它和HashMap的最大的不同是它是執行緒安全的,另外它不允許key和value為null。 > > Hashtable是個過時的集合類,不建議在新程式碼中使用,不需要執行緒安全的場合可以用HashMap替換,需要執行緒安全的場合可以用ConcurrentHashMap替換或者Collections的synchronizedMap方法使HashMap具有執行緒安全的能力。 | 不同點 | HashMap | HashTable | | ------------------------------- | ------------------- | ----------------------------------------- | | 資料結構 | 陣列+連結串列+紅黑樹 | 陣列+連結串列 | | 繼承的類不同 | 繼承AbstractMap | 繼承Dictionary | | 是否執行緒安全 | 否 | 是 | | 效能高低 | 高 | 低 | | 預設初始化容量 | 16 | 11 | | 擴容方式不同 | 原始容量*2 | 原始容量*2+1 | | 底層陣列的容量為2的整數次冪 | 要求為2的整數次冪 | 不要求 | | 確認key在陣列中的索引的方法不同 | i = (n - 1) & hash; | index = (hash & 0x7FFFFFFF) % tab.length; | | 遍歷方式 | Iterator(迭代器) | Iterator(迭代器)和Enumeration(列舉器) | | Iterator遍歷陣列順序 | 索引從小到大 | 索引從大到小 | ## 6 參考資料 >公眾號《Java3y》文章 > >知乎專欄《Java那些事兒》 > >[阿提說說](https://www.yuque.com/itsaysay/mzsmvg/1.java-based#1b98983d) > >[4.Java容器-HashMap詳解](https://blog.csdn.net/Regemc/article/details/79984489) > >[HashMap原始碼註解 之 靜態工具方法hash()、tableSizeFor()(四)](https://blog.csdn.net/fan2012huan/article/details/51097331) > >[HashMap中hash(Object key)原理,為什麼(hashcode >>> 16)。](https://blog.csdn.net/qq_42034205/article/details/90