本文快速回顧了Java中容器的知識點,用做面試複習,事半功倍。html
上篇:主要爲容器概覽,容器中用到的設計模式,List源碼java
中篇:Map源碼git
下篇:Set源碼,容器總結github
http://wiki.jikexueyuan.com/project/java-collection/hashmap.html面試
源碼分析:算法
http://www.javashuo.com/article/p-ykttmamr-gg.html編程
hashMap的一個內部類Node:segmentfault
1static class Node<K,V> implements Map.Entry<K,V> { 2 final int hash; 3 final K key; 4 V value; 5 Node<K,V> next; //鏈表結構,存儲下一個元素
在這裏插入圖片描述
Node內部包含了一個 Entry 類型的數組table,數組中的每一個位置被當成一個桶。設計模式
1transient Entry[] table;
Entry 存儲着鍵值對。它包含了四個字段,從 next 字段咱們能夠看出 Entry 是一個鏈表。即數組中的每一個位置被當成一個桶,一個桶存放一個鏈表。數組
HashMap 使用拉鍊法來解決衝突,同一個鏈表中存放哈希值相同的 Entry。
1static class Entry<K,V> implements Map.Entry<K,V> { 2 final K key; 3 V value; 4 Entry<K,V> next; 5 int hash; 6 7 Entry(int h, K k, V v, Entry<K,V> n) { 8 value = v; 9 next = n; 10 key = k; 11 hash = h; 12 } 13 14 public final K getKey() { 15 return key; 16 } 17 18 public final V getValue() { 19 return value; 20 } 21 22 public final V setValue(V newValue) { 23 V oldValue = value; 24 value = newValue; 25 return oldValue; 26 } 27 28 public final boolean equals(Object o) { 29 if (!(o instanceof Map.Entry)) 30 return false; 31 Map.Entry e = (Map.Entry)o; 32 Object k1 = getKey(); 33 Object k2 = e.getKey(); 34 if (k1 == k2 || (k1 != null && k1.equals(k2))) { 35 Object v1 = getValue(); 36 Object v2 = e.getValue(); 37 if (v1 == v2 || (v1 != null && v1.equals(v2))) 38 return true; 39 } 40 return false; 41 } 42 43 public final int hashCode() { 44 return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue()); 45 } 46 47 public final String toString() { 48 return getKey() + "=" + getValue(); 49 } 50}
在這裏插入圖片描述
構造時就會調用tableSizeFor():返回一個大於輸入參數且最近的2的整數次冪。
1static final int tableSizeFor(int cap) { 2 int n = cap - 1; 3 n |= n >>> 1; 4 n |= n >>> 2; 5 n |= n >>> 4; 6 n |= n >>> 8; 7 n |= n >>> 16; 8 return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; 9}
應該注意到鏈表的插入是以頭插法方式進行的
1HashMap<String, String> map = new HashMap<>(); 2map.put("K1", "V1"); 3map.put("K2", "V2"); 4map.put("K3", "V3");
查找須要分紅兩步進行:
計算鍵值對所在的桶;
在鏈表上順序查找,時間複雜度顯然和鏈表的長度成正比。
1public V put(K key, V value) { 2 if (table == EMPTY_TABLE) { 3 inflateTable(threshold); 4 } 5 // 鍵爲 null 單獨處理 6 if (key == null) 7 return putForNullKey(value); 8 int hash = hash(key); 9 // 肯定桶下標 10 int i = indexFor(hash, table.length); 11 // 先找出是否已經存在鍵爲 key 的鍵值對,若是存在的話就更新這個鍵值對的值爲 value 12 for (Entry<K,V> e = table[i]; e != null; e = e.next) { 13 Object k; 14 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { 15 V oldValue = e.value; 16 e.value = value; 17 e.recordAccess(this); 18 return oldValue; 19 } 20 } 21 22 modCount++; 23 // 插入新鍵值對 24 addEntry(hash, key, value, i); 25 return null; 26}
HashMap 容許插入鍵爲 null 的鍵值對。可是由於沒法調用 null 的 hashCode() 方法,也就沒法肯定該鍵值對的桶下標,只能經過強制指定一個桶下標來存放。HashMap 使用第 0 個桶存放鍵爲 null 的鍵值對。
1private V putForNullKey(V value) { 2 for (Entry<K,V> e = table[0]; e != null; e = e.next) { 3 if (e.key == null) { 4 V oldValue = e.value; 5 e.value = value; 6 e.recordAccess(this); 7 return oldValue; 8 } 9 } 10 modCount++; 11 addEntry(0, null, value, 0); 12 return null; 13}
使用鏈表的頭插法,也就是新的鍵值對插在鏈表的頭部,而不是鏈表的尾部。
1void addEntry(int hash, K key, V value, int bucketIndex) { 2 if ((size >= threshold) && (null != table[bucketIndex])) { 3 resize(2 * table.length); 4 hash = (null != key) ? hash(key) : 0; 5 bucketIndex = indexFor(hash, table.length); 6 } 7 8 createEntry(hash, key, value, bucketIndex); 9} 10 11void createEntry(int hash, K key, V value, int bucketIndex) { 12 Entry<K,V> e = table[bucketIndex]; 13 // 頭插法,鏈表頭部指向新的鍵值對 14 table[bucketIndex] = new Entry<>(hash, key, value, e); 15 size++; 16} 1Entry(int h, K k, V v, Entry<K,V> n) { 2 value = v; 3 next = n; 4 key = k; 5 hash = h; 6}
補充:hashmap裏hash方法的高位優化:
http://www.javashuo.com/article/p-wcboirew-cq.html
設計者將key的哈希值的高位也作了運算(與高16位作異或運算,使得在作&運算時,此時的低位其實是高位與低位的結合),這就增長了隨機性,減小了碰撞衝突的可能性!
爲什麼要這麼作?
table的長度都是2的冪,所以index僅與hash值的低n位有關,hash值的高位都被與操做置爲0了。
這樣作很容易產生碰撞。設計者權衡了speed, utility, and quality,將高16位與低16位異或來減小這種影響。設計者考慮到如今的hashCode分佈的已經很不錯了,並且當發生較大碰撞時也用樹形存儲下降了衝突。僅僅異或一下,既減小了系統的開銷,也不會形成的由於高位沒有參與下標的計算(table長度比較小時),從而引發的碰撞。
不少操做都須要先肯定一個鍵值對所在的桶下標。
1int hash = hash(key); 2int i = indexFor(hash, table.length);
4.1 計算 hash 值
1final int hash(Object k) { 2 int h = hashSeed; 3 if (0 != h && k instanceof String) { 4 return sun.misc.Hashing.stringHash32((String) k); 5 } 6 7 h ^= k.hashCode(); 8 9 // This function ensures that hashCodes that differ only by 10 // constant multiples at each bit position have a bounded 11 // number of collisions (approximately 8 at default load factor). 12 h ^= (h >>> 20) ^ (h >>> 12); 13 return h ^ (h >>> 7) ^ (h >>> 4); 14} 1public final int hashCode() { 2 return Objects.hashCode(key) ^ Objects.hashCode(value); 3}
4.2 取模
令 x = 1<\<\4,即 \x 爲 2 的 4 次方,它具備如下性質:
1x : 00010000 2x-1 : 00001111
令一個數 y 與 x-1 作與運算,能夠去除 y 位級表示的第 4 位以上數:
1y : 10110010 2x-1 : 00001111 3y&(x-1) : 00000010
這個性質和 y 對 x 取模效果是同樣的:
1y : 10110010 2x : 00010000 3y%x : 00000010
咱們知道,位運算的代價比求模運算小的多,所以在進行這種計算時用位運算的話能帶來更高的性能。
肯定桶下標的最後一步是將 key 的 hash 值對桶個數取模:hash%capacity,若是能保證 capacity 爲 2 的 n 次方,那麼就能夠將這個操做轉換爲位運算。
1static int indexFor(int h, int length) { 2 return h & (length-1); 3}
當 length 老是 2 的 n 次方時,h& (length-1)運算等價於對 length 取模,也就是 h%length,可是 & 比 % 具備更高的效率。這看上去很簡單,其實比較有玄機的,咱們舉個例子來講明:
<table>
從上面的例子中能夠看出:當它們和 15-1(1110)「與」的時候,8 和 9產生了相同的結果,也就是說它們會定位到數組中的同一個位置上去,這就產生了碰撞,8 和 9 會被放到數組中的同一個位置上造成鏈表,那麼查詢的時候就須要遍歷這個鏈 表,獲得8或者9,這樣就下降了查詢的效率。
同時,咱們也能夠發現,當數組長度爲 15 的時候,hash 值會與 15-1(1110)進行「與」,那麼最後一位永遠是 0,而 0001,0011,0101,1001,1011,0111,1101 這幾個位置永遠都不能存放元素了,空間浪費至關大,數組可使用的位置比數組長度小了不少,這意味着進一步增長了碰撞的概率。
因此說,當數組長度爲 2 的 n 次冪的時候,不一樣的 key 算得得 index 相同的概率較小,那麼數據在數組上分佈就比較均勻,也就是說碰撞的概率小
設 HashMap 的 table 長度爲 M,須要存儲的鍵值對數量爲 N,若是哈希函數知足均勻性的要求,那麼每條鏈表的長度大約爲 N/M,所以平均查找次數的複雜度爲 O(N/M)。
爲了讓查找的成本下降,應該儘量使得 N/M 儘量小,所以須要保證 M 儘量大,也就是說 table 要儘量大。HashMap 採用動態擴容來根據當前的 N 值來調整 M 值,使得空間效率和時間效率都能獲得保證。
和擴容相關的參數主要有:capacity、size、threshold 和 load_factor。
1static final int DEFAULT_INITIAL_CAPACITY = 16; 2 3static final int MAXIMUM_CAPACITY = 1 << 30; 4 5static final float DEFAULT_LOAD_FACTOR = 0.75f; 6 7transient Entry[] table; 8 9transient int size; 10 11int threshold; 12 13final float loadFactor; 14 15transient int modCount;
從下面的添加元素代碼中能夠看出,當須要擴容時,令 capacity 爲原來的兩倍。
1void addEntry(int hash, K key, V value, int bucketIndex) { 2 Entry<K,V> e = table[bucketIndex]; 3 table[bucketIndex] = new Entry<>(hash, key, value, e); 4 if (size++ >= threshold) 5 resize(2 * table.length); 6}
擴容使用 resize() 實現,須要注意的是,擴容操做一樣須要把 oldTable 的全部鍵值對從新插入 newTable 中,所以這一步是很費時的。
1void resize(int newCapacity) { 2 Entry[] oldTable = table; 3 int oldCapacity = oldTable.length; 4 if (oldCapacity == MAXIMUM_CAPACITY) { 5 threshold = Integer.MAX_VALUE; 6 return; 7 } 8 Entry[] newTable = new Entry[newCapacity]; 9 transfer(newTable); 10 table = newTable; 11 threshold = (int)(newCapacity * loadFactor); 12} 13 14void transfer(Entry[] newTable) { 15 Entry[] src = table; 16 int newCapacity = newTable.length; 17 for (int j = 0; j < src.length; j++) { 18 Entry<K,V> e = src[j]; 19 if (e != null) { 20 src[j] = null; 21 do { 22 Entry<K,V> next = e.next; 23 int i = indexFor(e.hash, newCapacity); 24 e.next = newTable[i]; 25 newTable[i] = e; 26 e = next; 27 } while (e != null); 28 } 29 } 30}
擴容-從新計算桶下標
Rehash優化:http://www.javashuo.com/article/p-edvgtahx-gy.html
在進行擴容時,須要把鍵值對從新放到對應的桶上。HashMap 使用了一個特殊的機制,能夠下降從新計算桶下標的操做。
假設原數組長度 capacity 爲 16,擴容以後 new capacity 爲 32:
1capacity : 00010000 2new capacity : 00100000
對於一個 Key,
總結:
通過rehash以後,元素的位置要麼是在原位置,要麼是在原位置再移動2次冪的位置
所以,咱們在擴充HashMap的時候,不須要像JDK1.7的實現那樣從新計算hash,只須要看看原來的hash值新增的那個bit是1仍是0就行了,是0的話索引沒變,是1的話索引變成「原索引+oldCap」,能夠看看下圖爲16擴充爲32的resize示意圖:
在這裏插入圖片描述
HashMap 構造函數容許用戶傳入的容量不是 2 的 n 次方,由於它能夠自動地將傳入的容量轉換爲 2 的 n 次方。
先考慮如何求一個數的掩碼,對於 10010000,它的掩碼爲 11111111,可使用如下方法獲得:
1mask |= mask >> 1 11011000 2mask |= mask >> 2 11111110 3mask |= mask >> 4 11111111 4
mask+1 是大於原始數字的最小的 2 的 n 次方。
1num 10010000 2mask+1 100000000
如下是 HashMap 中計算數組容量的代碼:
1static final int tableSizeFor(int cap) { 2 int n = cap - 1; 3 n |= n >>> 1; 4 n |= n >>> 2; 5 n |= n >>> 4; 6 n |= n >>> 8; 7 n |= n >>> 16; 8 return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; 9}
並非桶子上有8位元素的時候它就能變成紅黑樹,它得同時知足咱們的鍵值對大於64才行的
這是爲了不在哈希表創建初期,多個鍵值對剛好被放入了同一個鏈表中而致使沒必要要的轉化。
在這裏插入圖片描述
談談ConcurrentHashMap1.7和1.8的不一樣實現:
http://www.importnew.com/23610.html
詳細源碼分析(還未細看):
https://blog.csdn.net/yan_wenliang/article/details/51029372
http://www.javashuo.com/article/p-zggcwnug-bu.html
主要針對jdk1.7的實現來介紹
jdk1.7
jdk1.7中採用Segment + HashEntry的方式進行實現
在這裏插入圖片描述
Segment:其繼承於 ReentrantLock 類,從而使得 Segment 對象能夠充當鎖的角色。
Segment 中包含HashBucket的數組,其能夠守護其包含的若干個桶。
1static final class HashEntry<K,V> { 2 final int hash; 3 final K key; 4 volatile V value; 5 volatile HashEntry<K,V> next; 6}
ConcurrentHashMap採用了分段鎖,每一個分段鎖維護着幾個桶,多個線程能夠同時訪問不一樣分段鎖上的桶,從而使其併發度更高(併發度就是 Segment 的個數)。
jdk1.8
在這裏插入圖片描述
JDK 1.7 使用分段鎖機制來實現併發更新操做,核心類爲 Segment,它繼承自重入鎖 ReentrantLock,併發程度與 Segment 數量相等。
JDK 1.8 使用了 CAS 操做來支持更高的併發度,在 CAS 操做失敗時使用內置鎖 synchronized。
1.8中放棄了Segment臃腫的設計,取而代之的是採用Node數組 + CAS + Synchronized來保證併發安全進行實現
在這裏插入圖片描述
只讓一個線程對散列表進行初始化!
從頂部註釋咱們能夠讀到,get方法是不用加鎖的,是非阻塞的。
Node節點是重寫的,設置了volatile關鍵字修飾,導致它每次獲取的都是最新設置的值
每一個 Segment 維護了一個 count 變量來統計該 Segment 中的鍵值對個數。
在執行 size 操做時,須要遍歷全部 Segment 而後把 count 累計起來。
ConcurrentHashMap 在執行 size操做時先嚐試不加鎖,若是連續兩次不加鎖操做獲得的結果一致,那麼能夠認爲這個結果是正確的。
嘗試次數使用 RETRIES_BEFORE_LOCK 定義,該值爲 2,retries 初始值爲 -1,所以嘗試次數爲 3。
若是嘗試的次數超過 3 次,就須要對每一個 Segment 加鎖。
在這裏插入圖片描述
爲何用這麼方式刪除呢,細心的同窗會發現上面定義的HashEntry的key和next都是final類型的,因此不能改變next的指向,因此又複製了一份指向刪除的結點的next。
Collections.synchronizedMap()與ConcurrentHashMap的區別
參考:https://blog.csdn.net/lanxiangru/article/details/53495854
ConcurrentHashMap 的高併發性主要來自於三個方面:
http://wiki.jikexueyuan.com/project/java-collection/linkedhashmap.html
http://www.javashuo.com/article/p-fscfnemy-ct.html
在這裏插入圖片描述
在這裏插入圖片描述
該 Entry 除了保存當前對象的引用外,還保存了其上一個元素 before 和下一個元素 after的引用,從而在哈希表的基礎上又構成了雙向連接列表。
1/** 2* LinkedHashMap的Entry元素。 3* 繼承HashMap的Entry元素,又保存了其上一個元素before和下一個元素after的引用。 4 */ 5static class Entry<K,V> extends HashMap.Node<K,V> { 6 Entry<K,V> before, after; 7 Entry(int hash, K key, V value, Node<K,V> next) { 8 super(hash, key, value, next); 9 } 10 }
在這裏插入圖片描述
經過源代碼能夠看出,在 LinkedHashMap 的構造方法中,實際調用了父類 HashMap 的相關構造方法來構造一個底層存放的 table 數組,但額外能夠增長 accessOrder 這個參數,若是不設置
LinkedHashMap 重寫了父類 HashMap 的 get 方法,實際在調用父類 getEntry() 方法取得查找的元素後,再判斷當排序模式 accessOrder 爲 true 時,記錄訪問順序,將最新訪問的元素添加到雙向鏈表的表頭,並從原來的位置刪除。
因爲的鏈表的增長、刪除操做是常量級的,故並不會帶來性能的損失。
爲啥註釋說:初始容量對遍歷沒有影響?
由於它遍歷的是LinkedHashMap內部維護的一個雙向鏈表,而不是散列表(固然了,鏈表雙向鏈表的元素都來源於散列表)
http://wiki.jikexueyuan.com/project/java-collection/linkedhashmap-lrucache.html
LRU最近最少使用(訪問順序)
用這個類有兩大好處:
Java裏面實現LRU緩存一般有兩種選擇:
如下是使用 LinkedHashMap 實現的一個 LRU 緩存:
1class LRUCache<K, V> extends LinkedHashMap<K, V> { 2 private static final int MAX_ENTRIES = 3; 3 4 protected boolean removeEldestEntry(Map.Entry eldest) { 5 return size() > MAX_ENTRIES; 6 } 7 8 LRUCache() { 9 super(MAX_ENTRIES, 0.75f, true); 10 } 11} 1public static void main(String[] args) { 2 LRUCache<Integer, String> cache = new LRUCache<>(); 3 cache.put(1, "a"); 4 cache.put(2, "b"); 5 cache.put(3, "c"); 6 cache.get(1); 7 cache.put(4, "d"); 8 System.out.println(cache.keySet()); 9} 1[3, 1, 4]
實現詳細代碼請參考文章:補充知識點-緩存
FIFO(插入順序)
還能夠在插入順序的LinkedHashMap直接重寫下removeEldestEntry方法便可輕鬆實現一個FIFO緩存
在這裏插入圖片描述
詳細看:
http://www.javashuo.com/article/p-wumkjbiq-ng.html
總結:
本人目前爲後臺開發工程師,主要關注Python爬蟲,後臺開發等相關技術。
Csdn
擁有專欄:Leetcode題解(Java/Python)、Python爬蟲開發
知乎
https://www.zhihu.com/people/yang-zhen-dong-1/
擁有專欄:碼農面試助攻手冊
掘金
https://juejin.im/user/5b48015ce51d45191462ba55
簡書
https://www.jianshu.com/u/b5f225ca2376