本文一是總結前面兩種集合,補充一些遺漏,再對HashMap進行簡單介紹。
回顧
因為前兩篇ArrayList和LinkedList都是針對單獨的集合類分析的,只見樹木未見森林,今天分析HashMap,可以結合起來看一下java中的集合框架。下圖只是一小部分,而且為了方便理解去除了抽象類。
Java中的集合(有時也稱為容器)是為了儲存物件,而且多數時候儲存的不止一個物件。
可以簡單的將Java集合分為兩類:
-
一類是Collection,儲存的是獨立的元素,也就是單個物件。細分之下,常見的有List,Set,Queue。其中List保證按照插入的順序儲存元素。Set不能有重複元素。Queue按照佇列的規則來存取元素,一般情況下是“先進先出”。
-
一類是Map,儲存的是“鍵值對”,通過鍵來查詢值。比如現實中通過姓名查詢電話號碼,通過身份證號查詢個人詳細資訊等。
理論上說我們完全可以只用Collection體系,比如將鍵值對封裝成物件存入Collection的實現類,之所以提出Map,最主要的原因是效率。
HashMap簡介
HashMap用來儲存鍵值對,也就是一次儲存兩個元素。在jdk1.8中,其實現是基於陣列+連結串列+紅黑樹,簡單說就是普通情況直接用陣列,發生雜湊衝突時在衝突位置改為連結串列,當連結串列超過一定長度時,改為紅黑樹。
可以簡單理解為:在陣列中存放連結串列或者紅黑樹。
- 完全沒有雜湊衝突時,陣列每個元素是一個容量為1的連結串列。如索引0和1上的元素。
- 發生較小雜湊衝突時,陣列每個元素是一個包含多個元素的連結串列。如索引2上的元素。
- 當衝突數量超過8時,陣列每個元素是一棵紅黑樹。如索引6上的元素。
下圖為示意圖,相關結構沒有嚴格遵循規範。
類簽名
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;
上述變數的關鍵在於連結串列轉樹和樹轉連結串列的時機,綜合看:
- 當陣列的容量小於64是,此時不管衝突數量多少,都不樹化,而是選擇擴容。
- 當陣列的容量大於等於64時,
- 衝突數量大於8,則進行樹化。
- 當紅黑樹中元素數量小於6時,將樹轉為連結串列。
變數
//儲存節點的陣列,始終為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方法
上述方法中有一個非常巧妙的方法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方法。
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;
}
getNode方法
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值的方法,其實也很講究的。有機會再針對具體的細節慢慢詳細寫吧。