本文一是總結前面兩種集合,補充一些遺漏,再者對HashMap進行簡單介紹。java
由於前兩篇ArrayList和LinkedList都是針對單獨的集合類分析的,只見樹木未見森林,今天分析HashMap,能夠結合起來看一下java中的集合框架。下圖只是一小部分,並且爲了方便理解去除了抽象類。node
Java中的集合(有時也稱爲容器)是爲了存儲對象,並且多數時候存儲的不止一個對象。數組
能夠簡單的將Java集合分爲兩類:框架
一類是Collection,存儲的是獨立的元素,也就是單個對象。細分之下,常見的有List,Set,Queue。其中List保證按照插入的順序存儲元素。Set不能有重複元素。Queue按照隊列的規則來存取元素,通常狀況下是「先進先出」。函數
一類是Map,存儲的是「鍵值對」,經過鍵來查找值。好比現實中經過姓名查找電話號碼,經過身份證號查找我的詳細信息等。工具
理論上說咱們徹底能夠只用Collection體系,好比將鍵值對封裝成對象存入Collection的實現類,之因此提出Map,最主要的緣由是效率。this
HashMap用來存儲鍵值對,也就是一次存儲兩個元素。在jdk1.8中,其實現是基於數組+鏈表+紅黑樹,簡單說就是普通狀況直接用數組,發生哈希衝突時在衝突位置改成鏈表,當鏈表超過必定長度時,改成紅黑樹。debug
能夠簡單理解爲:在數組中存放鏈表或者紅黑樹。3d
下圖爲示意圖,相關結構沒有嚴格遵循規範。code
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
以下圖
實現Cloneable和Serializable接口,擁有克隆和序列化的能力。
HashMap繼承抽象類AbstractMap的同時又實現Map接口的緣由一樣見上一篇LinkedList。
//序列化版本號 private static final long serialVersionUID = 362498820763181265L; //默認初始化容量爲16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //最大容量,2的30次方 static final int MAXIMUM_CAPACITY = 1 << 30; //默認負載因子,值爲0.75 static final float DEFAULT_LOAD_FACTOR = 0.75f; //如下三個常量應結合看 //鏈表轉爲樹的閾值 static final int TREEIFY_THRESHOLD = 8; //樹轉爲鏈表的閾值,小於6時樹轉鏈表 static final int UNTREEIFY_THRESHOLD = 6; //鏈表轉樹時的集合最小容量。只有總容量大於64,且發生衝突的鏈表大於8才轉換爲樹。 static final int MIN_TREEIFY_CAPACITY = 64;
上述變量的關鍵在於鏈表轉樹和樹轉鏈表的時機,綜合看:
//存儲節點的數組,始終爲2的冪 transient Node<K,V>[] table; //批量存入時使用,詳見對應構造函數 transient Set<Map.Entry<K,V>> entrySet; //實際存放鍵值對的個數 transient int size; //修改map的次數,便於快速失敗 transient int modCount; //擴容時的臨界值,本質是capacity * load factor int threshold; //負載因子 final float loadFactor;
數組中存儲的節點類型,能夠看出,除了K和Value外,還包含了指向下一個節點的引用,正如一開始說的,節點實際是一個單向鏈表。
static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; //...省略常見方法 }
常見的無參構造和一個參數的構造很簡單,直接傳值,此處省略。看一下兩個參數的構造方法。
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; //將給定容量轉換爲不小於其自身的2的冪 this.threshold = tableSizeFor(initialCapacity); }
上述方法中有一個很是巧妙的方法tableSizeFor,它將給定的數值轉換爲不小於自身的最小的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; }
好比cap=10,轉換爲16;cap=32,則結果仍是32。用了位運算,保證效率。
有一個問題,爲啥非要把容量轉換爲2的冪?以前講到的ArrayList爲啥就不須要呢?其實關鍵在於hash,更準確的說是轉換爲2的冪,必定程度上減少了哈希衝突。
關於這些運算,畫個草圖很好理解,關鍵在於可以想到這個方法很牛啊。解釋的話配圖太多,這裏篇幅限制,將內容放在另外一篇文章。
在上面構造方法中,咱們沒有看到初始化數組也就是Node<K,V>[] table
的狀況,這一步驟放在了添加元素put時進行。
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
能夠看出put調用的是putVal方法。
在此以前回顧一下HashMap的構成,數組+鏈表+紅黑樹。數組對應位置爲空,存入數組,不爲空,存入鏈表,鏈表超載,轉換爲紅黑樹。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; //數組爲空,則擴容 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //根據key計算hash值得出數組中的位置i,位置i上爲空,直接添加。 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); //數組對應位置不爲空 else { Node<K,V> e; K k; //對應節點key上的key存在,直接覆蓋value 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) { 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) { V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; //下次添加前需不須要擴容,若容量已滿則提早擴容 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
resize()方法比較複雜,最好是配合IDE工具,debug一下,比較容易弄清楚擴容的方式和時機,若是幹講的話反而容易混淆。
根據鍵獲取對應的值,內部調用getNode方法
public V get(Object key) { Node<K,V> e; 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 && ((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; }
HashMap的內容太多,每一個內容相關的知識點也不少,篇幅和我的能力限制,很難講清全部內容,好比最基礎的獲取hash值的方法,其實也很講究的。有機會再針對具體的細節慢慢詳細寫吧。