把 Java 容器的學習筆記放到 github 裏了,還在更新~
其餘的目前不打算抽出來做爲文章寫,感受挖的還不夠深,等對某些東西理解的更深了再寫文章吧
Java 容器
目錄以下:java
Java 容器node
後面還會對併發、和一些 Java 基礎的東西作整理
爲啥要作那麼多筆記呢?我的比較喜歡把東西寫出來~嘻嘻數據結構
若是真的有人認真看了的話,要是有錯誤或者對我寫的感到迷惑的地方,再或者但願對哪些知識再深刻了解一些,請儘管說出來,給個人我的博客留言 or 發郵件 or 提 issue 都 ok,我會很是感謝你的~
我的博客連接多線程
看一下官方文檔中對HashMap的描述併發
* Hash table based implementation of the <tt>Map</tt> interface. This * implementation provides all of the optional map operations, and permits * <tt>null</tt> values and the <tt>null</tt> key. (The <tt>HashMap</tt> * class is roughly equivalent to <tt>Hashtable</tt>, except that it is * unsynchronized and permits nulls.) This class makes no guarantees as to * the order of the map; in particular, it does not guarantee that the order * will remain constant over time.
HashMap 底層的數據結構就是數組+鏈表+紅黑樹,紅黑樹是在 JDK 1.8 中加進來的。尤爲是在 JDK 1.8 對它優化之後,HashMap 變成了一個更強的容器...嗯...真的很強。
當新建一個 HashMap 時,就會初始化一個數組。在這個數組中,存放的是 Node 類,它擁有指向單獨的一個鏈表的頭結點的引用,這個鏈表是用來解決 hash 衝突的(若是不一樣的 key 被映射到數組中同一位置的話,就將其放入鏈表中,從而解決衝突)。
大概就是這樣子... ⁄(⁄ ⁄•⁄ω⁄•⁄ ⁄)⁄ 數組 __ |__| 鏈表 __ __ __ __ __ |__|---> |__|->|__|->|__|->|__|->... __ |__| __ |__| __ |__| __ |__| : Node<K, V>
可是,在 JDK 1.8 以前的這種作法,即便負載因子和 Hash 算法設計的再合理,也沒法避免會出現鏈表過長的狀況, 一旦鏈表過長,會嚴重影響 HashMap 的性能,因此,在 JDK 1.8 以後,使用了紅黑樹這個數據結構,當鏈表長度大於 8 時,該鏈表就會轉化成紅黑樹,利用紅黑樹快速增刪查改的特色提升 HashMap 的性能。
由於 HashMap 是不一樣步的,若是須要考慮線程安全,須要使用 ConcurrentHashMap,或者可使用 Collections.synchronizedMap() 方法返回被指定 map 支持的同步的 map。
Map<Integer, Integer> map = Collections.synchronizedMap(new HashMap<>());
// 默認初始容量是16,必須是2的冪 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 // 最大容量(必須是2的冪且小於2的30次方,傳入容量過大會被這個值替換) static final int MAXIMUM_CAPACITY = 1 << 30; // 默認加載因子,加載因子就是指哈希表在其容量自動增長以前能夠達到多滿的一種尺度 static final float DEFAULT_LOAD_FACTOR = 0.75f; // 默認的轉換成紅黑樹的閾值,即鏈表長度達到該值時,該鏈表將轉換成紅黑樹 static final int TREEIFY_THRESHOLD = 8; // 存儲Entry的默認空數組 static final Entry<?,?>[] EMPTY_TABLE = {}; // 存儲Entry的數組,長度爲2的冪。HashMap採用拉鍊法實現的, // 每一個Entry的本質是個單向鏈表 transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE; // HashMap的大小,即HashMap中實際存在的鍵值對數量 transient int size; // 閾值,表示所能容納的key-value對的極限,用於判斷是否須要調整HashMap的容量 // 若是 table 仍是空的,那麼這個閾值就是 0 或者是默認的容量 16 int threshold; // 加載因子實際大小 final float loadFactor; // HashMap被修改的次數,用於 fail-fast 機制 transient int modCount;
其中須要特別注意的是capacity和load factor這兩個屬性
官方文檔中對其描述是:
The capacity is the number of buckets in the hash table, and the initial capacity is simply the capacity at the time the hash table is created. The load factor is a measure of how full the hash table is allowed to get before its capacity is automatically increased.
load factor(負載因子):哈希表中的填滿程度。
Node 實際上就是一個單鏈表,它實現了Map.Entry接口,其中next也是一個Node對象,用來處理hash衝突,造成一個鏈表。
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; } // 判斷兩個node是否equal(必須key和value都相等) 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; } }
HashMap 有四個構造函數
/** 用指定的初始容量和負載因子建立HashMap */ 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); } /** 用指定的容量建立HashMap,負載因子爲默認的0.75 */ public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } /** 均使用默認值(初始容量:16 默認負載因子:0.75) */ public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted } /** 用指定的一個 map 構造一個新HashMap 新的 HashMap 的負載因子爲默認值 0.75,容量爲足以裝載該 map 的容量,會在 putMapEntries 中設置 */ public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); }
肯定位置這部分是很重要的,不管增刪查鍵值對,首先都要定位到哈希桶數組的位置!理想的狀況就是數組中每一個位置都只有一個元素,這樣在用算法求得這個位置後,咱們就能直接命中該元素,不用再遍歷鏈表了,這樣能夠極大地優化查找的效率.
在源碼中,採用的方法就是先根據 hashCode 先計算出 hash 值,而後根據 hash 值再求得索引,從而找到位置。
求hash值
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
(">>>"爲按位右移補零操做符。左操做數的值按右操做數指定的位數右移,移動獲得的空位以零填充。)
hash 值的計算主要分三步:
計算索引
// 此處取的put方法片斷,這裏就是用(n - 1) & hash 計算的索引(n爲表的長度) if ((p = tab[i = (n - 1) & hash]) == null)
計算方法其實就是取模運算。
對於計算索引的取模運算,是一個很是很是巧妙的運算~ ヽ(✿゚▽゚)ノ
它是用 hash & (n - 1) 獲得索引值,由於 HashMap 底層數組的長度老是 2 的 n 次方(這是 HashMap 在速度上的優化),經過下面這個函數去保證 table 的長度爲 2 的次冪。
// 這個靜態函數的做用就是返回一個比 cap 大可是又最接近 cap 的 2 次冪的整數 // 原理就是經過不斷地 位或 和 按位右移補零 操做, // 將 n 變成 0..0111..111 這種形式,最後 + 1,就變成了 2 的次冪 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; }
有了這個前提: n 必定爲 2 的 n 次方,那麼這個表達式才能等價於 hash % n,爲何不直接用 hash % n 呢?由於 & 比 % 具備更高的效率呀,因此採用的是 hash & (n - 1) 而不是 hash % n。
那麼爲何n 爲 2 的 n 次方時 hash & (n - 1) 能夠等價於對 n 取模呢?
我是這樣想的
首先,n,即鏈表長度,爲 2 的 n 次方,那麼 n 就能夠表示成 100...00 的這種樣子,那麼 n - 1 就是 01111...11。
所以,HashMap 的做者要求鏈表的長度必須爲 2 的整數次冪,應該就是爲了這樣能使不一樣 hash 值發生碰撞的機率較小,讓元素在哈希表中均勻的散列。
put的過程大體是:
public V put(K key, V value) { // 計算hash值 return putVal(hash(key), key, value, false, true); } final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { // tab爲哈希表數組,p爲咱們要找的那個插入位置的節點 Node<K,V>[] tab; Node<K,V> p; int n, i; // 若tab爲空,就建立一個(進行擴容操做) if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //根據key計算hash值並處理後獲得索引,若是表的這個位置爲空,則直接插入 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); // 若是不爲空 else { Node<K,V> e; K k; // 判斷key是否存在,若是存在,則直接覆蓋其value if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; // 判斷該鏈表是否爲TreeNode,若是是,紅黑樹直接插入鍵值對 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); // 若是不是,則遍歷鏈表,而後插入 else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == 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; } } // 插入 if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; // 若是超過閾值,則進行擴容 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
get方法和put方法過程相似
public V get(Object key) { Node<K,V> e; // 計算hash值 return (e = getNode(hash(key), key)) == null ? null : e.value; } final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> 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) { // 在紅黑樹中get if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); // 在鏈表中get do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }
爲何要進行 resize?
何時進行resize?
下面是對 resize 源碼的分析,這段我以爲是最艱難的一段。。這還跳過了紅黑樹 :(´□`」 ∠):
final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; 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; } // 若是沒超過最大值,而且假設容量 double 後也不超過最大值, // 那就擴容爲原來的 2 倍, // 而後再看原來的容量是否是還夠 // 若是不夠了,閾值再 double,不然只是擴容,不改變閾值 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } // 從閾值中取出 resize 應該擴容的值 else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; // oldCap = 0 else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // 計算新的閾值 if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; // 建立一個新的 table @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; // 指向新的 table table = newTab; if (oldTab != null) { // 把每一個bucket都移動到新的buckets中 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) // 將樹上的結點 rehash,而後放到新位置,紅黑樹這塊之後在分析 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // preserve order // 進行鏈表複製 // lo鏈的新索引值和之前相同 // hi鏈的新索引值爲:原索引值 + oldCap Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { next = e.next; /** (e.hash & oldCap) == 0 這個地方比較難理解,但也是擴容最關鍵的地方 假設如今 (e.hash & oldCap) == 0 爲 true oldCap 和 new Cap 確定都是 2 的次冪,也就是 100... 這種形式,那麼假如如今 oldCap = 16, 那麼原索引爲 e.hash & (oldCap - 1) = e.hash & 01111 --> index ① 新的索引爲 e.hash & (newCap - 1) = e.hash & 11111 同時咱們已知 e.hash & oldCap = 0, 即 e.hash & 10000 = 0 ② 經過 ① ② 就能夠推出 e.hash & 11111 --> index 即索引位置不變 */ 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; }
擴容部分完結撒花 ★,°:.☆( ̄▽ ̄)/$:.°★
HashMap 不是線程安全的,它在被設計的時候就沒有考慮線程安全,由於這原本就不是一個併發容器,相應的併發容器是 ConcurrentHashMap,那麼,HashMap 的線程不安全性主要體如今哪兒呢?
最著名的一個就是高併發環境下的死循環問題,具體是在 resize 時產生的。
這種死循環產生的主要緣由是由於 1.7 的 resize 中,新的 table 採用的插入方式是隊頭插入(LIFO,後進先出),好比元素爲 {3,5,7,9},插入後就是 {9,7,5,9},會將鏈表順序逆置,它這樣作主要是爲了防止遍歷鏈表尾部,由於 resize 原本就是建立了一個新的 table,因此對於元素的順序不關心,所以採用隊頭插入的方式,若是是正常的從尾部插入的話,還須要先找到尾部的位置,增長了遍歷的消耗,而 resize 又正好不在意元素順序,因此就使用的隊頭插入的方式。
可是這種方式帶來了一個問題,就是死循環,具體死循環怎麼產生的我就不贅述了,由於網上有不少關於這個的具體分析,我要說的是,在 JDK 1.8 中,HashMap 除了加入了紅黑樹這個數據結構外還有一些其餘的調整,在 resize 時對鏈表的操做,變成了兩對指針分別對 lo鏈 和 hi鏈 操做。
Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null;
由於增長了xxTail指針,因此能夠隨時找到尾部,避免遍歷尾部,所以能夠直接在尾部插入,於是避免了死循環問題。
不過這不表明 JDK 1.8 的HashMap就是線程安全了的,由於很明顯還存在好比並發時元素的覆蓋之類的問題,因此多線程環境下仍是建議使用 ConcurrentHashMap 或者進行同步操做。