(1)時間複雜度:用來衡量算法的運行時間。
參考:https://blog.csdn.net/qq_41523096/article/details/82142747node
(2)數組:採用一段連續的存儲空間來存儲數據。查找方便,增刪麻煩。面試
(3)鏈表:採用一段不連續的存儲空間存儲數據,每一個數據中都存有指向下一條數據的指針。即 n 個節點離散分配,彼此經過指針相連,每一個節點只有一個前驅節點,每一個節點只有一個後續節點。增刪方便,查找麻煩,算法
(4)紅黑樹:一種自平衡的二叉查找樹,時間複雜度 O(log n)。segmentfault
(5)散列表、哈希表:結合數組 與 鏈表的優勢。經過 散列函數 計算 key,並將其映射到 散列表的 某個位置(連續的存儲空間)。對於相同的 hash 值(產生 hash 衝突),一般採用 拉鍊法來解決。簡單地講,就是將 hash(key) 獲得的結果 做爲 數組的下標,若多個key 的 hash(key) 相同,那麼在當前數組下標的位置創建一個鏈表來保存數據。數組
(6)HashMap:基於 哈希表的 Map 接口的非同步實現(即線程不安全),提供全部可選的映射操做。底層採用 數組 + 鏈表 + 紅黑樹的形式,容許 null 的 Key 以及 null 的 Value。不保證映射的順序且不保證順序恆久不變。安全
(1)採用 數組 + 鏈表的形式。
HashMap 採用 Node 數組來存儲 key-value 鍵值對,且數組中的每一個 Node 其實是一個單向的鏈表,內部存儲下一個 Node 實體的指針。數據結構
transient Node<K,V>[] table; static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; Node(int hash, K key, V value, Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } public final K getKey() { return key; } public final V getValue() { return value; } public final String toString() { return key + "=" + value; } public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } public final boolean equals(Object o) { if (o == this) return true; if (o instanceof Map.Entry) { Map.Entry<?,?> e = (Map.Entry<?,?>)o; if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue())) return true; } return false; } }
(2)當前數組長度大於某個閾值(默認爲 64),且鏈表長度大於某個閾值(默認爲 8)時,鏈表會轉爲 紅黑樹。多線程
/** * 初始數組容量,必須爲 2 的整數次冪。默認爲 2^4 = 16 */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; /** * 最大數組容量, 默認爲 2^30。 */ static final int MAXIMUM_CAPACITY = 1 << 30; /** * 負載因子,默認爲 0.75。 * 用於計算 HashMap 容量。 */ static final float DEFAULT_LOAD_FACTOR = 0.75f; /** * 樹化的第一個條件: * 鏈表轉紅黑樹的閾值,默認爲 8。 * 即鏈表長度大於等於 8 時,當前鏈表會轉爲紅黑樹進行存儲。 */ static final int TREEIFY_THRESHOLD = 8; /** * 紅黑樹轉鏈表的閾值,默認爲 6。 * 即紅黑樹節點小於等於 6 時,當前紅黑樹會轉爲鏈表進行存儲。 */ static final int UNTREEIFY_THRESHOLD = 6; /** * 樹化的第二個條件: * 樹化最小容量,默認爲 64。 * 當前數組長度大於等於 64 時,才能夠進行 鏈表轉紅黑樹。 */ static final int MIN_TREEIFY_CAPACITY = 64 /** * 數組,用於存儲 Node<K, V> 鏈表 */ transient Node<K,V>[] table; /** * 用於存儲 Node<K, V> 的總個數 */ transient int size; /** * 數組長度閾值,當超過該值後,會調整數組的長度。通常經過 capacity * load factor 計算 */ int threshold; /** * 負載因子,用於計算閾值,默認爲 0.75 */ final float loadFactor; /** * 用於快速失敗(fail-fast)機制,當對象結構被修改後會改變。 */ transient int modCount;
(1)源碼:併發
/** * 經常使用無參構造方法,以默認值構造 HashMap。 */ public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted } /** * HashMap 核心構造方法,根據 初始化容量 以及 負載因子建立 HashMap. * @param initialCapacity 初始化容量 * @param loadFactor 負載因子 * @throws IllegalArgumentException 非法數據異常 */ public HashMap(int initialCapacity, float loadFactor) { // 若是初始化容量 小於 0 ,則會拋出 非法數據 異常 if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); // 若是初始化容量 大於 最大容量值,則給其賦值爲最大值 if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; // 若負載因子小於 0 或者 不合法, 拋出 非法數據異常 if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); // 若上述條件均成立,則保存 負載因子的值 this.loadFactor = loadFactor; // 若上述條件均成立,則保存 數組長度的閾值(2的整數次冪)。 this.threshold = tableSizeFor(initialCapacity); } 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; }
(2)分析:
上例的構造函數,根據 初始化容量以及 負載因子去建立 HashMap,沒有去 實例化 Node 數組,數組的實例化 須要在 put 方法裏實現。
數組長度閾值 經過 tableSizeFor() 方法實現,能返回一個比給定容量大的 且 最小的 2 的次冪的數。好比 initialCapacity = 21, tableSizeFor() 返回的結果爲 32。app
用於計算 key 的 hash 值。
(1)源碼:
/** * 計算 key 的 hash 值的方法 */ static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } // Node 數組 transient Node<K,V>[] table; // 獲取某個 key 所在位置時,經過 (table.length - 1) & hash(key) 去計算數組下標 table[(table.length - 1) & hash(key)]
(2)分析
採用 高 16 位 與 低 16 位 異或,而後再進行移位運算。主要是爲了減小衝突。
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } (length - 1) & hash(key) 【舉例:】 假設某個值通過 hashCode 計算後爲: 1111 0101 1010 0101 1101 1110 0000 0000 數組長度爲 16,那麼 length -1 = 15,以下: 0000 0000 0000 0000 0000 0000 0000 1111 此時進行 (length - 1) & hash(key) 操做後, 1111 0101 1010 0101 1101 1110 0000 0000 & 0000 0000 0000 0000 0000 0000 0000 1111 = 0000 0000 0000 0000 0000 0000 0000 0000 即只要 hashCode 計算出的值最後四位爲0,獲得的結果就必定爲 0,此時衝突會大大提升。 採用 高16位 與 低16位 異或,計算爲: 1111 0101 1010 0101 1101 1110 0000 0000 ^ 0000 0000 0000 0000 1111 0101 1010 0101 = 1111 0101 1010 0101 0010 1011 1010 0101 此時進行 (length - 1) & hash(key) 操做後, 1111 0101 1010 0101 0010 1011 1010 0101 & 0000 0000 0000 0000 0000 0000 0000 1111 = 0000 0000 0000 0000 0000 0000 0000 0101 此時計算出來的,是hashcode結果的後幾位的值,這樣就能夠減小衝突的發生。
方法做用:
Step1: 給 HashMap 的數組 初始化。
Step2: 定義 鏈表 轉爲 紅黑樹的條件。
Step3: 定義數據存儲的動做(存儲的方式:鏈表仍是紅黑樹)。
(1)分析 put 過程
Step1:put 內部調用 putVal() 方法。
Step2:先判斷 數組是否爲 null 或者 長度爲0,是的話,則調用 resize 方法給數組擴容。
Step3:對 key 進行 hash 並執行位運算((length - 1) & hash(key)),獲得數組下標。若不衝突,即當前數組位置不存在元素,直接在此處添加一個節點便可。
Step4:若衝突,即當前數組位置存在元素,則根據節點的狀況進行判斷。
若是 剛好是第一個 元素,則進行替換 value 的操做。
若是不是第一個元素,則判斷是否爲 紅黑樹結構,是則添加一個樹節點。
若是不是紅黑樹結構(即鏈表),則採用尾插法給鏈表插入一個節點,鏈表長度大於等於 8 時,將鏈表轉爲紅黑樹結構。
Step5:若 Node 長度大於閾值,還得從新 resize 擴容。
(2)源碼:
// Node 數組 transient Node<K,V>[] table; // 插入數據的操做 public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } /** * 真正的插入數據的方法。 * @param key 的 hash 值 * @param key * @param value * @param onlyIfAbsent爲 true,插入數據若存在值時,不會進行修改操做 * @param evict if false, the table is in creation mode. * @return 上一個值,若不存在,則返回 null */ final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; // 若是 Node 數組爲 null 或者 長度爲 0 時,即 Node 數組不存在,則調用 resize() 方法,從新獲取一個調整大小後的 Node 數組。 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 若是當前數組元素沒有值,即不存在 哈希衝突的狀況,直接添加一個 Node 進去(多線程時,此處可能致使線程不安全)。 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { // 存在哈希衝突的狀況下,須要找到 插入或修改 的節點的位置,而後再操做(插入或修改) Node<K,V> e; K k; // Step1:找到節點的位置1 // 判斷第一個節點 是否是咱們須要找的,判斷條件: hash 是否相等、 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 { // 是鏈表節點,遍歷查找節點 for (int binCount = 0; ; ++binCount) { // 第一個條件判斷得知 第一個節點不是咱們要的,因此能夠直接從第二個節點開始(p.next),而後遍歷得第3、四個節點。 if ((e = p.next) == null) { // 若是第二(3、四。。。)個節點沒有值,直接添加一個 Node 便可,此時的 e 爲 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; } } // 當 e 不爲 null 時,對值進行修改,並將舊值返回 if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; // 此處是 空實現,LinkedHashMap使用 afterNodeAccess(e); return oldValue; } } // 添加節點的後續操做 // 修改次數加1 ++modCount; // 當Node節點數 size 大於 閾值時,須要執行 resize 方法調整數組長度。 if (++size > threshold) resize(); // 此處是 空實現,LinkedHashMap使用 afterNodeInsertion(evict); // 添加節點成功,返回 null return null; }
用於給數組擴容。
(1)resize 過程
Step1:計算新數組的閾值、新數組的長度。
Step2:給新數組複製。對於鏈表節點採用 e.hash & oldCap 去肯定元素的位置,新位置只有兩種可能(在原位置、或者在原位置的基礎上增長 舊數組長度)
【舉例:】 e.hash = 10 = 0000 1010, oldCap = 16 = 0001 0000 則 e.hash & oldCap = 0000 0000 = 0 e.hash = 18 = 0001 0010, oldCap = 16 = 0001 0000 則 e.hash & oldCap = 0001 0000 = 16 當 e.hash & oldCap == 0 時,新位置爲 原數據所在的位置。即 table[j] 當 e.hash & oldCap != 0 時,新位置爲 原數據所在的位置 + 原數組的長度。即 table[j + oldCap]
(2)源碼:
/** * 給數組擴容 */ final Node<K,V>[] resize() { // Step1:判斷數組是否須要擴容,若須要則擴容 // 記錄原數組 Node<K,V>[] oldTab = table; // 記錄原數組長度,若爲 null,則爲 0, 不然爲 數組的長度 int oldCap = (oldTab == null) ? 0 : oldTab.length; // 記錄原數組的閾值 int oldThr = threshold; // 記錄新數組的長度、閾值 int newCap, newThr = 0; // 若是原數組已被初始化 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 } // 原數組爲null,若舊閾值大於0, 則數組長度爲 閾值大小 else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; // 原數組爲 null,舊閾值小於等於0, 則數組長度、閾值均爲默認值 else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // 若新閾值爲 0,則根據新數組長度從新計算閾值 if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; // Step2:將原數組的數據複製到新數組中(從新計算元素新的位置) @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; // 對數組的每一個節點進行判斷 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; // 判斷節點是否須要移動,位運算 爲 0 則不移動 if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } // 位運算不爲 0,需移動 else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); // 將鏈表的尾節點置 null,並將頭節點放到新位置 if (loTail != null) { loTail.next = null; // 新位置爲 原始位置 newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; // 新位置爲 原始位置 + 原始數組長度 newTab[j + oldCap] = hiHead; } } } } } return newTab; }
用於獲取節點的 value 值。
(1)分析 get 的過程
Step1:先獲取節點,內部調用 getNode() 方法。
Step2:判斷 數組是否爲 null 或者 長度爲0,是則直接返回 null。對 key 進行 hash 並執行位運算((length - 1) & hash(key)),獲得數組下標,若當前數組下標位置數據爲null,也返回 null。
Step3:若當前數組下標位置有值。
若 剛好是第一個元素,直接返回第一個節點便可。
若不是第一個元素,則判斷是否爲 紅黑樹結構,是則返回樹節點。
若不是樹結構,則遍歷鏈表,返回相應的節點。
(2)源碼:
/** * 根據 key 獲取 value */ public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; } /** * 真正獲取 value 的操做 */ final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; // 數組長度爲 0 或者爲 null,或者 節點不存在,直接返回一個 null 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<K,V>)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); } } return null; }
數組用於 肯定 數據存儲的位置,鏈表用來解決 哈希衝突(當衝突時,在當前數組對應的位置造成一個鏈表)。當鏈表的長度大於等於 8 時,須要將其轉爲 紅黑樹,查詢效率比鏈表高。
採用數組 + 鏈表的數據結構,能夠結合 數組尋址的優點 以及 鏈表在增刪上的高效。
當數組長度超過閾值時(loadFactor * capacity),默認負載因子(loadFactor) 爲 0.75,數組(capacity)長度 爲 16。
此時的閾值爲 16 * 0.75 = 12,即只要數組長度大於 12 時,就會發生擴容(resize)。數組、閾值擴大到原來的 2 倍。即 當前數組長度爲 16,擴容後變爲 32,閾值爲 24。
爲了實現高效、必須減小碰撞,即須要將數據儘可能均勻分配,使每一個鏈表長度大體相同。數據 key 的哈希值直接使用確定是不行的,能夠採用 取模運算 ,即 hash(key) % length,獲得的餘數做爲數組的下標( table[hash(key) % length] )。
可是取模運算的效率沒有 移位運算高((length - 1) & hash(key))。length 指的是數組的長度。
// Node 數組 transient Node<K,V>[] table; JDK 1.8 源碼給的實現是 (length - 1) & hash(key), // 計算數組下標值 table[(length - 1) & hash(key)] // 定位到數組元素的位置 也即 (length - 1) & hash(key) == hash(key) % length, 想要上面等式成立, length 必須知足 2 的次冪(效率最高), 即 length = 2^n。 爲何必須知足 2 的次冪? 由於只有 2 的次冪, length - 1 的二進制位全爲1,使得 hash(key) 後幾位都進行 &1 操做, 這樣獲得的結果等同於 hash(key) 後幾位的值。 即 (length - 1) & hash(key) == hash(key) % length 若是 不爲 2 的次冪,那麼可能存在 某些值永遠都不會出現的狀況。 舉個例子: 【hash(key) = 9, length = 16】 此時 hash(key) % length = 9 % 16 = 9 (length - 1) & hash(key) = 15 & 9 = 1111 & 1001 = 1001 = 9 hash(key) % length == (length - 1) & hash(key) 【hash(key) = 27, length = 16】 此時 hash(key) % length = 27 % 16 = 11 (length - 1) & hash(key) = 15 & 27 = 01111 & 11011 = 1011 = 11 hash(key) % length == (length - 1) & hash(key) 【hash(key) = 9, length = 15】 此時 hash(key) % length = 9 % 15 = 9 (length - 1) & hash(key) = 14 & 9 = 1110 & 1001 = 1000 = 8 hash(key) % length !== (length - 1) & hash(key) 數組長度爲 15 時,length -1 = 1110,此時無論如何,最後一位均不可能爲 1,也即 100一、1101等這些值永遠都獲取不到。
參考:https://segmentfault.com/a/1190000010799123。
以 31 爲權,對每個字符的 ASCII 碼進行運算。
選用 31 的緣由,31 * i = 32 * i - i = (i << 5) - i,31 能夠被虛擬機優化成 位運算,效率更高。
public int hashCode() { int h = hash; if (h == 0 && value.length > 0) { char val[] = value; for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; } hash = h; } return h; }
HashMap 採用尾插法將數據插入鏈表的尾部,但其 putVal 方法是線程不安全的。putVal 方法中有段代碼以下:
if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null);
當線程 A 與線程 B 同時進行 put 操做時,且兩個值的 key 通過 hash() 是一致的,即佔用同一個數組元素。若此時數組元素爲 null,線程 A 執行到這段代碼的時候,發現該位置數據爲 null,則觸發一次 newNode 操做,這時線程 B 剛好也執行到這,一樣觸發一次 newNode 操做,這時不論是線程 A 仍是線程 B成功,都會覆蓋當前元素,即線程不安全。
JDK 7 用的頭插法,會形成死循環(沒有過多研究,有時間再補充)。
(1)HashMap 是線程非安全的,容許存在 null 的 key 以及 null 的 value。且只有一個爲 null 的key,能夠存在多個爲 null 的 value。HashMap 的效率比 HashTable 高(2)HashTable 是線程安全的,不容許存在 null 值。(3)ConcurrentHashMap 是線程安全的 HashMap,併發能力比 HashTable 強。