# 添加元素的過程 向HashSet添加元素a,首先調用元素a所在類的hashCode()方法,計算元素a的哈希值, 此哈希值接着經過某種算法計算出在HashSet底層數組中的存放位置,判斷數組此位置上是否已經有元素: 若是此位置上沒有其餘元素,則元素a添加成功. ---> 狀況1 若是此位置上有其餘元素b(或以鏈表形式存在的多個元素),則比較元素a與元素b的hash值: 若是hash值不相同,則元素a添加成功. ---> 狀況2 若是hash值相同,進而須要調用元素a所在類的equals()方法: equals()返回true,元素a添加失敗 equals()返回false,則元素a添加成功. --->狀況2 - 對於添加成功的狀況2和狀況3而言:元素a與已存在指定索引位置上數據以鏈表的方式存儲. jdk 7: 元素a放到數組中,指向原來的元素. jdk 8: 原來的元素在數組中,指向新放入的元素a
Map實現的底層原理:java
Map是個鏈表數組,數組中的每一個元素都是一個Map.Entry對象,同時Entry對象是個鏈表的節點,在這個鏈表的節點上能夠經過next指向他的下一個節點。node
1.7的結構以下:算法
# 1 Map實現類的結構 - |---Map:存儲鍵值對的數據 ---相似於高中的函數 y=f(x) |---HashMap:做爲Map的主要實現類;線程不安全 可是效率高;存儲null的k和v |---LinkedHashMap:保證在遍歷map元素時,能夠按照添加的順序實現遍歷. 緣由:在原有的HashMap底層結構基礎上,添加了一對指針,指向前一個元素和後一個元素 |---TreeMap:保證按照添加的k-v對進行排序,實現排序遍歷.此時考慮key的天然排序或定製排序 底層使用紅黑樹 |---HashTable:做爲古老的實現類:線程安全,效率低;不能存儲null的key和value |---Properties:經常使用來處理配置文件.key和value都是String類型 * HashMap的底層: 數組+鏈表(jdk 7 以前) 數組+鏈表+紅黑樹 (jdk 8) # 2 Map結構的理解 Map中的key : 無序的、不可重複的,使用Set存儲全部的key ---> 當使用hasnMap時key所在的類要重寫equals()和hashCode() Map中的value:無序的、不可重複的、使用Collection存儲全部的value 一個鍵值對:key-value構成了一個Entry對象 Map中的entry : 無序的、不可重複的,使用Set存儲全部的entry # 3 HashMap的底層實現原理 在實例化之後,底層建立了長度是16的一維數組Entry[] table. ...執行屢次put後... map.put(key1,value1); 1). 首先調用key1所在類的hashCode() 計算key哈希值,此哈希值通過某種算法計算之後,獲得在Entry數組中的存放位置. 2). 若是此位置上的數據爲空,此時的key1-value1添加成功 ---> 狀況一 若是此位置上的數據不爲空,(意味着此位置上存在一個或多個數據(以鏈表形式存在)),比較key1和已經存在的一個或多個數據的哈希值: 3). 若是key1的哈希值與已經存在的數據的哈希值都不相同,此時key1-value1添加成功 ---> 狀況2 若是key1的哈希值和已經存在的某一個數據(key2-value2)的哈希值相同,繼續比較,調用key1所在類的equals(key2)方法: 若是equals()返回false:此時key1-value1添加成功 --->狀況3 若是equals()返回true:使用value1替換value2.而且return value2 - 補充:關於狀況2和狀況3:此時key1-value1和原來的數據以鏈表的方式存儲. - 在不斷額添加過程當中,會涉及到擴容問題,當超出臨界值(平衡因子 * Entry[]的長度)而且將要放入的元素非空時擴容,擴容後須要從新計算hash,並從新擺放Entry的位置 默認的擴容方式:擴容爲原來的2倍,並將原有的數據複製過來. # 4 jdk8 相較於jdk7在底層實現方面的不一樣: 1. new HashMap():底層沒有建立一個長度爲16的數組(與ArrayList類似) 2. jdk8底層的數組是:Node[],而非Entry[] 3. 首次調用put() 方法時,底層建立長度爲16的數組 4. jdk7 底層結構只有:數組+鏈表.jdk8中底層結構:數組+鏈表+紅黑樹. 當數組的某一個索引位置上的元素以鏈表形式存在的數據個數 > 8 且當前數組的長度 > 64 時, 此時索引位置上的全部數據改成使用紅黑樹存儲. > jdk7中createEntry的過程(往鏈表插入節點的方式) 採用頭插法,即將新元素放到數組頭上,而後舊的鏈表連到新元素後面 > jdk8中createEntry的過程 > 採用尾插法,即將新元素放到數組列表的後面,由於引入紅黑樹以後,就須要判斷單鏈表的節點個數(超過8個後要轉成紅黑樹),因此乾脆使用尾插法,正好遍歷單鏈表,讀取節點個數.也正是由於尾插法,使得HashMap在插入節點時,能夠判斷是否有重複節點.
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; //引用當前hashMap的散列表 Node<K,V> p; //表示當前散列表的元素 int n, i; //n: 當前散列表的長度 i: 尋址結果 //延遲初始化邏輯,第一次調用putVAl時會初始化hashMap對象中的最耗費內存的散列表 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //狀況1: 尋址到的位置恰好是null,這個時候,直接put if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { //e: 不爲null的話,找到了一個與當前要插入的key-value一致的key的元素 k: 臨時的key Node<K,V> e; K k; //表示桶位中的該元素,與你當前插入的元素的key徹底一致,表示後續須要進行替換操做 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //紅黑樹的狀況操做 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { //鏈表的狀況,並且鏈表的頭元素與咱們要插入的key不一致 //binCount記錄鏈表的長度 for (int binCount = 0; ; ++binCount) { //開始遍歷鏈表,而且沒找到一個與要插入的key一致的node if ((e = p.next) == null) { //鏈表尾插節點 p.next = newNode(hash, key, value, null); //若是說鏈表長度超過了閾值,就變成紅黑樹 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } //找到了相同的key的node元素,須要進行替換操做 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } //找到相同key的替換操做 if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } //modCount: 表示散列表結構被修改的次數,替換Node元素的value不算 ++modCount; //size自增,並判斷是否是須要擴容 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
final Node<K,V>[] resize() { //oldTab: 引用擴容前的哈希表 Node<K,V>[] oldTab = table; //oldCap: 擴容前數組長度 int oldCap = (oldTab == null) ? 0 : oldTab.length; //oldThr: 擴容前的閾值 int oldThr = threshold; //擴容以後的數組長度和閾值 int newCap, newThr = 0; //若是散列表已經初始化過了,正常擴容 if (oldCap > 0) { //若是已經達到了散列表的最大長度將再也不擴容 if (oldCap >= MAXIMUM_CAPACITY) { //將閾值設爲最大值 threshold = Integer.MAX_VALUE; return oldTab; } //新的最大值爲舊的最大值翻倍 //16 = 0000 1000 左移一位後 0001 0000 = 32 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) //若是說你設置的初始大小小於了默認大小16,將不會進行擴大閾值 newThr = oldThr << 1; // double threshold } //散列表未初始化的狀況 oldCap == 0 說明hashMap中的散列表是null 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); } //newThr爲0時,計算出一個newThr if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; //根據以前計算的cap,創造出一個更大的數組 @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; //若是擴容以前有數據 if (oldTab != null) { //遍歷散列表 for (int j = 0; j < oldCap; ++j) { Node<K,V> e; //當前node節點 //若是說這個桶位有數據(多是單個節點,也多是鏈表或者是紅黑樹) if ((e = oldTab[j]) != null) { //將舊的桶位置空,方便GC回收內存 oldTab[j] = null; //1. 單個節點的狀況 if (e.next == null) //從新計算hash值,並放入該位置 newTab[e.hash & (newCap - 1)] = e; //2. 紅黑樹的狀況 else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); //3. 鏈表的狀況 /** * 以hash值爲15的桶爲例,擴容前的hash值爲: 1111 * 擴容後的hash值可能爲: 01111 或者 11111 兩種狀況 */ else { // preserve order //低位鏈表: 存放在擴容以後的數組的下標位置,與當前數組的下標位置相同 //例如 01111 存放在原位置 Node<K,V> loHead = null, loTail = null; //高位鏈表: 存放在擴容以後的數組的下標位置爲: 當前數組下標的位置 + oldCap // 11111 存放在 31號桶 Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { next = e.next; //hash-> ....1 1111 & 1 0000 = 1 //hash-> ....0 1111 & 0 0000 = 0 if ((e.hash & oldCap) == 0) { //低位鏈表初始化 if (loTail == null) loHead = e; //低位鏈表添加next節點 else loTail.next = e; loTail = e; } else { //高位鏈表初始化 if (hiTail == null) hiHead = e; //高位鏈表添加next節點 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; //放到當前數組下標的位置 + oldCap newTab[j + oldCap] = hiHead; } } } } } return newTab; }
final Node<K,V> getNode(int hash, Object key) { //tab: 引用當前hashMap的散列表 Node<K,V>[] tab; //first: 桶位中的頭元素 e: 臨時node節點 Node<K,V> first, e; //n: 散列表長度 int n; K k; //若是說當前散列表不爲空,而且當前桶有數據 if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { //1. 頭節點爲要查找的元素,則直接返回 if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; //若是說當前桶位不止一個元素 if ((e = first.next) != null) { //2. 若是說當前桶位是樹的狀況 if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); //3. 若是說當前桶位造成了鏈表就遍歷查找元素 do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }
/** * Implements Map.remove and related methods * * @param hash hash for key * @param key the key * @param value the value to match if matchValue, else ignored * @param matchValue if true only remove if value is equal * @param movable if false do not move other nodes while removing * @return the node, or null if none */ final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) { //tab: 引用當前hashMap中的散列表 //p: 當前node元素 //index: 尋址結果 Node<K,V>[] tab; Node<K,V> p; int n, index; //當前散列表不爲空,而且所要刪除元素的桶位也不爲空 if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) { //node: 查找到的結果 e: 當前node的下一個元素 Node<K,V> node = null, e; K k; V v; //1. 找到了匹配的元素 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) node = p; //若是說當前桶位不止一個元素 else if ((e = p.next) != null) { //2. 桶位是樹的狀況 if (p instanceof TreeNode) node = ((TreeNode<K,V>)p).getTreeNode(hash, key); //3. 桶位是鏈表的狀況 else { //遍歷鏈表,找到要刪除的元素 do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { node = e; break; } p = e; } while ((e = e.next) != null); } } //若是node不爲空的話,說明按照key查找到須要刪除的數據了 if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) { //若是是樹的狀況,執行樹節點的移除 if (node instanceof TreeNode) ((TreeNode<K,V>)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; }
Q0:HashMap是如何定位下標的?
A:先獲取Key,而後對Key進行hash,獲取一個hash值,而後用hash值對HashMap的容量進行取餘(實際上不是真的取餘,而是使用按位與操做,緣由參考Q6),最後獲得下標。數組
Q1:HashMap由什麼組成?
A:數組+單鏈表,jdk1.8之後又加了紅黑樹,當鏈表節點個數超過8個(m默認值)而且容量大於64之後,開始使用紅黑樹,使用紅黑樹一個綜合取優的選擇,相對於其餘數據結構,紅黑樹的查詢和插入效率都比較高。而當紅黑樹的節點個數小於6個(默認值)之後,又開始使用鏈表。安全
這兩個閾值爲何不相同呢?markdown
主要是爲了防止出現節點個數頻繁在一個相同的數值來回切換,舉個極端例子,如今單鏈表的節點個數是9,開始變成紅黑樹,而後紅黑樹節點個數又變成8,就又得變成單鏈表,而後節點個數又變成9,就又得變成紅黑樹,這樣的狀況消耗嚴重浪費,所以乾脆錯開兩個閾值的大小,使得變成紅黑樹後「不那麼容易」就須要變回單鏈表,一樣,使得變成單鏈表後,「不那麼容易」就須要變回紅黑樹。數據結構
Q2:Java的HashMap爲何不用取餘的方式存儲數據?
A:實際上HashMap的indexFor方法用的是跟HashMap的容量-1作按位與操做,而不是%求餘。(這裏有個硬性要求,容量必須是2的指數倍,緣由參考Q6)app
Q3:HashMap往鏈表裏插入節點的方式?
A:jdk1.7之前是頭插法,jdk1.8之後是尾插法,由於引入紅黑樹以後,就須要判斷單鏈表的節點個數(超過8個後要轉換成紅黑樹),因此乾脆使用尾插法,正好遍歷單鏈表,讀取節點個數。也正是由於尾插法,使得HashMap在插入節點時,能夠判斷是否有重複節點。函數
Q4:HashMap默認容量和負載因子的大小是多少?
A:jdk1.7之前默認容量是16,負載因子是0.75。源碼分析
Q5:HashMap初始化時,若是指定容量大小爲10,那麼實際大小是多少?
A:16,由於HashMap的初始化函數中規定容量大小要是2的指數倍,即2,4,8,16,因此當指定容量爲10時,實際容量爲16。
源碼以下:
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; }
☆☆☆☆Q6:容量大小爲何要取2的指數倍?
A:兩個緣由:1,提高計算效率:由於2的指數倍的二進制都是隻有一個1,而2的指數倍-1的二進制就都是左全0右全1。那麼跟(2^n - 1)作按位與運算的話,獲得的值就必定在【0,(2^n - 1)】區間內,這樣的數就剛合適能夠用來做爲哈希表的容量大小,由於往哈希表裏插入數據,就是要對其容量大小取餘,從而獲得下標。因此用2^n作爲容量大小的話,就能夠用按位與操做替代取餘操做,提高計算效率。2.便於動態擴容後的從新計算哈希位置時能均勻分佈元素:由於動態擴容仍然是按照2的指數倍,因此按位與操做的值的變化就是二進制高位+1,好比16擴容到32,二進制變化就是從0000 1111(即15)到0001 1111(即31),那麼這種變化就會使得須要擴容的元素的哈希值從新按位與操做以後所得的下標值要麼不變,要麼+16(即挪動擴容後容量的一半的位置),這樣就能使得本來在同一個鏈表上的元素均勻(相隔擴容後的容量的一半)分佈到新的哈希表中。(注意:緣由2(也能夠理解成優勢2),在jdk1.8以後才被發現並使用)
Q7:HashMap知足擴容條件的大小(即擴容閾值)怎麼計算?
A:擴容閾值=min(容量負載因子,MAXIMUM_CAPACITY+1),MAXIMUM_CAPACITY很是大,因此通常都是取(容量負載因子)
Q8:HashMap是否支持元素爲null?
A:支持。
☆☆☆Q9:HashMap的 hash(Obeject k)方法中爲何在調用 k.hashCode()方法得到hash值後,爲何不直接對這個hash進行取餘,而是還要將hash值進行右移和異或運算?
源碼以下:
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
A:若是HashMap容量比較小而hash值比較大的時候,哈希衝突就容易變多。基於HashMap的indexFor底層設計,假設容量爲16,那麼就要對二進制0000 1111(即15)進行按位與操做,那麼hash值的二進制的高28位不管是多少,都沒意義,由於都會被0&,變成0。因此哈希衝突容易變多。那麼hash(Obeject key)方法中在調用 k.hashCode()方法得到hash值後,進行的一步運算:(h = key.hashCode()) ^ (h >>> 16)有什麼用呢?
首先,(h = key.hashCode()) ^ (h >>> 16)是將h的二進制中高位右移變成低位。其次異或運算是利用了特性:同0異1原則,儘量的使得key.hashCode()) ^ (h >>> 16)在未來作取餘(按位與操做方式)時都參與到運算中去。綜上,簡單來講,經過key.hashCode()) ^ (h >>> 16);運算,可使k.hashCode()方法得到的hash值的二進制中高位儘量多地參與按位與操做,從而減小哈希衝突。
Q10:哈希值相同,對象必定相同嗎?對象相同,哈希值必定相同嗎?
A:不必定。必定。
Q11:HashMap的擴容與插入元素的順序關係?
A:jdk1.7之前是先擴容再插入,jdk1.8之後是先插入再擴容。
Q12:HashMap擴容的緣由?
A:提高HashMap的get、put等方法的效率,由於若是不擴容,鏈表就會愈來愈長,致使插入和查詢效率都會變低。
Q13:jdk1.8引入紅黑樹後,若是單鏈表節點個數超過8個,是否必定會樹化?
A:不必定,它會先去判斷是否須要擴容(即判斷當前節點個數是否大於擴容的閾值),若是知足擴容條件,直接擴容,不會樹化,由於擴容不只能增長容量,還能縮短單鏈表的節點數,一箭雙鵰。