2.1 HashMap
2.1.1 HashMap介紹
先看看HashMap類頭部的源碼:
public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
HashMap基於哈希表的 Map 接口的實現。此實現提供所有可選的映射操作,並允許使用 null 值和 null 鍵。(除了非同步和允許使用 null 之外,HashMap 類與 Hashtable 大致相同。)此類不保證映射的順序,特別是它不保證該順序恆久不變。
此實現假定哈希函數將元素適當地分布在各個桶(數組元素)之間,可為基本操作(get 和 put)提供穩定的性能。迭代 collection 視圖所需的時間與 HashMap 實例的“容量”(桶的數量)及其大小(鍵-值映射關系數)成比例。所以,如果迭代性能很重要,則不要將初始容量設置得太高(或將負載因子設置得太低)。
HashMap 的實例有兩個參數影響其性能:初始容量和負載因子。容量是哈希表中桶的數量,初始容量只是哈希表在創建時的容量。負載因子是哈希表在其容量自動增加之前可以達到多滿的一種尺度。當哈希表中的條目數超出了加載因子與當前容量的乘積時,則要對該哈希表進行 rehash 操作(即重建內部數據結構),從而哈希表將具有大約兩倍的桶數。
通常,默認加載因子 (.75) 在時間和空間成本上尋求一種折衷。加載因子過高雖然減少了空間開銷,但同時也增加了查詢成本(在大多數HashMap 類的操作中,包括get 和put 操作,都反映了這一點)。在設置初始容量時應該考慮到映射中所需的條目數及其加載因子,以便最大限度地減少rehash 操作次數。如果初始容量大於最大條目數除以加載因子,則不會發生rehash 操作。
如果很多映射關系要存儲在 HashMap 實例中,則相對於按需執行自動的 rehash 操作以增大表的容量來說,使用足夠大的初始容量創建它將使得映射關系能更有效地存儲。
注意,此實現不是同步的。如果多個線程同時訪問一個哈希映射,而其中至少一個線程從結構上修改了該映射,則它必須保持外部同步。(結構上的修改是指添加或刪除一個或多個映射關系的任何操作;僅改變與實例已經包含的鍵關聯的值不是結構上的修改。)這一般通過對自然封裝該映射的對象進行同步操作來完成。如果不存在這樣的對象,則應該使用 Collections.synchronizedMap 方法來“包裝”該映射。最好在創建時完成這一操作,以防止對映射進行意外的非同步訪問,如下所示:
Map m = Collections.synchronizedMap(new HashMap(...));
由所有此類的“collection 視圖方法”所返回的迭代器都是快速失敗的:在迭代器創建之后,如果從結構上對映射進行修改,除非通過迭代器本身的remove 方法,其他任何時間任何方式的修改,迭代器都將拋出 ConcurrentModificationException。因此,面對並發的修改,迭代器很快就會完全失敗,而不會在將來不確定的時間發生任意不確定行為的風險。
注意,迭代器的快速失敗行為不能得到保證,一般來說,存在非同步的並發修改時,不可能作出任何堅決的保證。快速失敗迭代器盡最大努力拋出 ConcurrentModificationException。因此,編寫依賴於此異常的程序的做法是錯誤的,正確做法是:迭代器的快速失敗行為應該僅用於檢測程序錯誤。
2.1.2 HashMap存儲結構圖
這里先給出HashMap的存儲結構,在后面的源碼分析中,我們將更加詳細的對此作介紹。HashMap采取數組加鏈表的存儲方式來實現。亦即數組(散列桶)中的每一個元素都是鏈表,如下圖:
圖2-1
說明:下面針對HashMap的源碼分析中,所有提到的桶或散列桶都表示存儲結構中數組的元素,桶或散列桶的數量亦即表示數組的長度,哈希碼亦即散列碼。
2.1.3 屬性分析
先來看看HashMap有哪些屬性,HashMap沒有從AbstractMap父親中繼承任何屬性,下面這些都是HashMap的屬性:
static final int DEFAULT_INITIAL_CAPACITY = 16;
DEFAULT_INITIAL_CAPACITY是HashMap默認的初始化桶數量,如圖2-1中所示。對於HashMap中桶數量的值必須是2的N次冪,而且這個是HashMap強制規定的。這樣做的原因就是因為計算機進行2次冪的運算是非常高效的,僅通過位移操作就可以完成2的N次冪的運算。
static final int MAXIMUM_CAPACITY = 1 << 30;
MAXIMUM_CAPACITY是HashMap中散列桶數量的最大值,從上面的代碼可知這個最大值為2的32次冪,即1073741824。
static final float DEFAULT_LOAD_FACTOR = 0.75f;
默認的負載因子,如果在在創建HashMap的構造函數中沒有指定負載因子,則指定該HashMap的默認負載因子為0.75,這意味着當HashMap中條目的數量達到了條目數量75%時,HashMap將進行resize操作以增加桶的數量。對於桶的擴展,等分析到下面的具體時會作更詳細的介紹。
transient Entry<K,V>[] table;
table就是HashMap的存儲結構,顯然這是一個數組,數組的每一個元素都是一個條目(Entry),Entry是HashMap中的一個內部類,它有如下4個屬性:final K key;V value;Entry<K,V> next;int hash。分別為鍵、值、指向下一個鏈表結點的指針、散列(哈希)值。這就是圖2.1中HashMap存儲結構的代碼實現。
transient int size;
size表示HashMap中條目(即鍵-值對)的數量。
int threshold;
threshold是HashMap的重構閾值,它的值為容量和負載因子的乘積。在HashMap中所有桶中條目的總數量達到了這個重構閾值之后,HashMap將進行resize操作以自動擴容。
final float loadFactor;
loadFactor表示HashMap的負載因子,它和容量一樣都是HashMap擴容的決定性因素。
transient int modCount;
modCount表示HashMap被結構化更新的次數,比如插入、刪除、清空等會更新HashMap結構的操作次數。
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT
= Integer.MAX_VALUE;
ALTERNATIVE_HASHING_THRESHOLD_DEFAULT表示在對字符串鍵(即key為String類型)的HashMap應用備選哈希函數時HashMap的條目數量的默認閾值。備選哈希函數的使用可以減少由於對字符串鍵進行弱哈希碼計算時的碰撞概率。
transient boolean useAltHashing;
useAltHashing表示是否要對字符串鍵的HashMap使用備選哈希函數。
transient final int hashSeed = sun.misc.Hashing.randomHashSeed(this);
hashSeed表示一個與當前實例關聯並且可以減少哈希碰撞概率應用於鍵的哈希碼計算的隨機種子。
2.1.4 構造分析
HashMap提供了4個構造方法,按照它們在源碼中的位置順序從上至下列出:
HashMap(int initialCapacity, float loadFactor)
HashMap(int initialCapacity)
HashMap()
HashMap(Map<? extends K, ? extends V> m)
(1) 我們先來分析第一個同時傳遞初始化容量參數和負載因子參數的源碼,因為其它的3個構造方法都會調用這個構造方法,下面給出這個方法的代碼及分析:
public HashMap(int initialCapacity, float loadFactor) {
//部分構造參數容錯處理的源碼已省略...
/**
* 根據傳入的初始化容量計算該HashMap的容量(即桶的數量)
* 算法為:將capacity進行不斷的左移,直至capacity大於或等於初始化容量
*/
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
//負載因子初始化
this.loadFactor = loadFactor;
/**
* 條目閾值的計算
* 算法:超出條目最大容量前取容量與負載因子的乘積作為條目閾值
*/
threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
//創建數組(散列桶)
table = new Entry[capacity];
//計算是否對字符串鍵的HashMap使用備選哈希函數
useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
init();//調用初始化方法,默認情況下什么也沒做
}
(2) 下面是只傳初始化容量參數的構造方法:
public HashMap(int initialCapacity) {
//初始化容量傳入,加載因子為默認值0.75f
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
(3) 下面是無參構造方法:
public HashMap() {
//初始化容量為默認值16,加載因子也為默認值0.75f
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
(4) 下面是根據已有Map構造新HashMap的構造方法:
public HashMap(Map<? extends K, ? extends V> m) {
/**
* 取下面兩個值的較大的值作為當前要構造的HashMap的初始容量
* 第1個值:用傳入的Map的條目數量除以默認加載因子再加上1
* 第2個值:默認的初始化容量
*/
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
/**
* 把傳入的map里的所有條目放入當前已構造的HashMap中
* 關於putAllForCreate方法后面會作分析
*/
putAllForCreate(m);
}
2.1.5 hash方法
hash方法的源碼及分析如下:
final int hash(Object k) {
int h = 0;
/**
* 如果useAltHashing的值為true
* 並且鍵的類型為String,則對字符串鍵使用備選哈希函數
* 否則,返回用於對鍵進行哈希碼計算的隨機種子hashSeed
* 關於hashSeed在2.1.3.1小節中已介紹過,這里不再贅述
*/
if (useAltHashing) {
if (k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h = hashSeed;
}
/**
* 對h和鍵的哈希碼進行抑或並賦值運算
* 等價於h = h ^ k.hashCode();
*/
h ^= k.hashCode();
//下面兩步的運算過程如圖2-2所示
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
假設h=0x7FFFFFFF,則上面最后兩步對h的運算過程如下圖:
圖2-2
2.1.6 indexFor方法
/**
* h表示通過hash(Object k)方法計算得來的哈希碼
* length表示桶的數量(即數組的長度)
*/
static int indexFor(int h, int length) {
/**
* 將哈希碼和length進行按位與運算
* 所有的h值都會在映射在閉區間[0,length-1]內
* 不同的h值可能映射到閉區間[0,length-1]內同一個值上
*/
return h & (length-1);
}
2.1.7 put方法
/**
* 在HashMap中存儲一個鍵值對,若指定的鍵已經存在於HashMap中
* 則將新的值替換掉舊值,否則新添加一個條目來存儲這個鍵值對
* @param key 指定的鍵
* @param value 指定的值
* @return 若該鍵已經存在則返回該鍵對應的舊值,否則返回null
*/
public V put(K key, V value) {
if (key == null)
/**
* 若鍵為null,則調用putForNullKey方法進行插入
* putForNullKey的源碼這里不再分析,讀者有興趣可以自行分析它的源碼
*/
return putForNullKey(value);
//下面這兩個方法在前面兩小節中已經分析過
int hash = hash(key);//計算鍵對應的哈希碼
int i = indexFor(hash, table.length);//計算桶的索引
//遍歷桶中所有的元素(即鏈表的結點)
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
//桶中已經存在指定的鍵,替換指定鍵對應的舊值並返回該舊值
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
/**
* 桶中不存在指定鍵,則調用addEntry方法添加向桶中添加新結點
* addEntry方法下一小節將會詳細介紹
*/
addEntry(hash, key, value, i);
return null;
}
2.1.8 addEntry方法
/**
* 向HashMap的指定桶中添加一個新的鍵對值
* 若要對HashMap擴容(即增加桶的數量),則下面的方法可能會修改傳入的桶索引
* @param hash 指定鍵對應的哈希碼
* @param key 指定鍵
* @param value 指定值
* @param bucketIndex 桶索引
*/
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
//如果HashMap中條目的數量達到了重構閾值且指定的桶不為null,則對HashMap進行擴容(即增加桶的數量)
/**
* 調用resize方法對HashMap進行擴容
* 對於resize方法,下面會有專門的一小節來作介紹,這里先不介紹
*/
resize(2 * table.length);
//擴容后,桶的數量增加了,故需要重新對鍵進行哈希碼的計算
hash = (null != key) ? hash(key) : 0;
//根據新的鍵哈希碼和新的桶數量重新計算桶索引值
bucketIndex = indexFor(hash, table.length);
}
/**
* 在指定的桶中創建一個新的條目以存儲我們傳入的鍵值對
* 對於createEntry方法,讀者若有興趣可以自行閱讀其源碼
*/
createEntry(hash, key, value, bucketIndex);
}
2.1.9 resize方法
/**
* 重新調整HashMap中桶的數量
* @param newCapacity 新的桶數量
*/
void resize(int newCapacity) {
/**
* 下面的這段代碼對新值進行判斷
* 如果新值超過了條目(Entry)數量的最大值
* 則新int最大值賦值給重構閾值然后,然后直接返回而不會進行擴容
*/
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
//若newCapacity合法,則新建一個桶數組。
Entry[] newTable = new Entry[newCapacity];
//計算是否需要對鍵重新進行哈希碼的計算
boolean oldAltHashing = useAltHashing;
useAltHashing |= sun.misc.VM.isBooted() &&
(newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
boolean rehash = oldAltHashing ^ useAltHashing;
/**
* 將原有所有的桶遷移至新的桶數組中
* 在遷移時,桶在桶數組中的絕對位置可能會發生變化
* 這就是為什么HashMap不能保證存儲條目的順序不能恆久不變的原因
* 讀者若有興趣,可以自行閱讀transfer方法的源碼
*/
transfer(newTable, rehash);
//將新的桶數組的引用賦值給舊數組
table = newTable;
//像構造方法中一樣來重新計算重構閾值
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
2.1.10 get方法
/**
* 根據指定鍵獲取該鍵對應的值
* @param key 指定鍵
* @return 若該鍵存在於HashMap中,則返回該鍵對應的值,否則返回null
*/
public V get(Object key) {
if (key == null)
//若鍵為null,則返回null鍵對應的值
return getForNullKey();
//根據鍵獲取條目,下一小節會單獨介紹getEntry方法
Entry<K,V> entry = getEntry(key);
//返回條目的值,若條目為null,則返回null
return null == entry ? null : entry.getValue();
}