1. 相關數據結構
- HashMap概括的講就是數組+線性鏈表,首先回顧一下HashMap涉及到的三種數據結構。
-
- 數組:一組連續的內存存儲數據,根據下標的查找複雜度爲O(1),根據給定的值查找複雜度爲O(n)。(查找快,插入刪除慢)。
- 線性鏈表:如果能直接定位,新增和刪除只需要O(1)的複雜度,但是查找定位需要遍歷,平均複雜度爲O(logn)(查找慢,插入刪除快)。
- 哈希表:在哈希表中進行添加,刪除,查找等操作,性能十分之高,不考慮哈希衝突的情況下,僅需一次定位即可完成,時間複雜度爲O(1)。
哈希表的主幹是數組,因爲根據哈希函數得出索引後能夠直接在數組上一次定位;但是如果發生哈希衝突(根據hash得到的地址已經被佔用),則需要另外的解決方法,解決哈希衝突的方法一般是開放地址法以及鏈地址法。開放地址法是指衝突之後繼續找下一塊可用地址,而鏈地址法則是引入鏈表作爲數組的子結構繼續存儲數據。
2. HashMap的結構
- HashMap的主幹是一個Entry數組。Entry是HashMap的基本組成單元,每一個Entry包含一個key-value鍵值對。數組是HashMap的主體,鏈表則是主要爲了解決哈希衝突而存在的。
- Entry是HashMap中的一個靜態內部類。代碼如下
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;//存儲指向下一個Entry的引用,單鏈表結構
int hash;//對key的hashcode值進行hash運算後得到的值,存儲在Entry,避免重複計算
/**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
- 整個HashMap的結構如下:
- 如果定位到的數組位置不含鏈表(當前entry的next指向null),那麼對於查找,添加等操作很快,僅需一次尋址即可;如果定位到的數組包含鏈表,對於添加操作,其時間複雜度依然爲O(1),因爲最新的Entry會插入鏈表頭部,只需要簡單改變引用鏈即可,而對於查找操作來講,此時就需要遍歷鏈表,然後通過key對象的equals方法逐一比對查找。所以,性能考慮,HashMap中的鏈表出現越少,性能纔會越好。
2. HashMap的構造器
- HashMap的常規構造方法如下:
public HashMap(int initialCapacity, float loadFactor) { //此處對傳入的初始容量進行校驗,最大不能超過MAXIMUM_CAPACITY = 1<<30(230)
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;
threshold = initialCapacity;
init();//init方法在HashMap中沒有實際實現,不過在其子類如 linkedHashMap中就會有對應實現
}
- 結合HashMap的構造方法,我們可以看到有兩個參數可以影響HashMap的性能:初始容量(inital capacity,初始爲16)和負載係數(load facto,初始爲0.75)。初始容量指定了初始table的大小,負載係數用來指定自動擴容的臨界值。當entry的數量超過capacity*load_factor時,容器將自動擴容並重新哈希。對於插入元素較多的場景,將初始容量設大可以減少重新哈希的次數。
3. HashMap的get(Object key)
- get(Object key)方法根據指定的key值返回對應的value,該方法調用了getEntry(Object key)得到相應的entry,然後返回entry.getValue()。因此getEntry()是算法的核心。源碼如下:
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
//通過key的hashcode值計算hash值
int hash = (key == null) ? 0 : hash(key);
//indexFor (hash&length-1) 獲取最終數組索引,然後遍歷鏈表,通過equals方法比對找出對應記錄
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
- 算法思想是首先通過hash()函數得到對應bucket的下標,然後依次遍歷衝突鏈表,通過key.equals(k)方法來判斷是否是要找的那個entry。
4. 關鍵:hashCode()與equals()
- 將對向放入到HashMap或HashSet中時,有兩個方法需要特別關心:hashCode()和equals()。hashCode()方法決定了對象會被放到哪個bucket裏,當多個對象的哈希值衝突時,equals()方法決定了這些對象是否是“同一個對象”。所以,如果要將自定義的對象放入到HashMap或HashSet中,需要@Override hashCode()和equals()方法。
5. HashMap的put(Object key,Object value)
- 當程序試圖將一個key-value對放入HashMap中時,程序首先根據該 key 的 hashCode() 返回值決定該 Entry 的存儲位置:如果兩個 Entry 的 key 的 hashCode() 返回值相同,那它們的存儲位置相同(數組下標相同)。如果這兩個 Entry 的 key 通過 equals 比較返回 true,新添加 Entry 的 value 將覆蓋集合中原有 Entry 的 value,但key不會覆蓋。如果這兩個 Entry 的 key 通過 equals 比較返回 false,新添加的 Entry 將與集合中原有 Entry 形成 Entry 鏈,而且新添加的 Entry 位於 Entry 鏈的頭部(頭插法)。源碼如下:
public V put(K key, V value) {
//如果table數組爲空數組{},進行數組填充(爲table分配實際內存空間),入參爲threshold,此時threshold爲initialCapacity 默認是1<<4(24=16)
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
//如果key爲null,存儲位置爲table[0]或table[0]的衝突鏈上
if (key == null)
return putForNullKey(value);
int hash = hash(key);//對key的hashcode進一步計算,確保散列均勻
int i = indexFor(hash, table.length);//獲取在table中的實際位置
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
//如果該對應數據已存在,執行覆蓋操作。用新value替換舊value,並返回舊value
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++;//保證併發訪問時,若HashMap內部結構發生變化,快速響應失敗
addEntry(hash, key, value, i);//新增一個entry
return null;
}
4. HashMap的擴容(resize)
- 當HashMap中的元素越來越多的時候,hash衝突的機率也就越來越高,因爲數組的長度是固定的。所以爲了提高查詢的效率,就要對HashMap的數組進行擴容,數組擴容這個操作也會出現在ArrayList中,這是一個常用的操作,而在HashMap數組擴容之後,最消耗性能的點就出現了:原數組中的數據必須重新計算其在新數組中的位置,並放進去,這就是resize。
- 那麼HashMap什麼時候進行擴容呢?當HashMap中的元素個數超過數組大小*loadFactor時,就會進行數組擴容,loadFactor的默認值爲0.75,這是一個折中的取值。也就是說,默認情況下,數組大小爲16,那麼當HashMap中元素個數超過16*0.75=12的時候,就把數組的大小擴展爲 2*16=32,即擴大一倍,然後重新計算每個元素在數組中的位置,而這是一個非常消耗性能的操作,所以如果我們已經預知HashMap中元素的個數,那麼預設元素的個數能夠有效的提高HashMap的性能。
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
transfer(newTable, initHashSeedAsNeeded(newCapacity));
//transfer方法逐個遍歷鏈表,重新計算索引位置,將老數組數據複製到新數組中去
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
5. HashMap的性能參數
- HashMap 包含如下幾個構造器:
- HashMap():構建一個初始容量爲 16,負載因子爲 0.75 的 HashMap
- HashMap(int initialCapacity):構建一個初始容量爲 initialCapacity,負載因子爲 0.75 的 HashMap。
- HashMap(int initialCapacity, float loadFactor):以指定初始容量、指定的負載因子創建一個 HashMap。
- initialCapacity:HashMap的最大容量,即爲底層數組的長度。
- loadFactor:負載因子loadFactor定義爲:散列表的實際元素數目(n)/ 散列表的容量(m)。
- 負載因子衡量的是一個散列表的空間的使用程度,負載因子越大表示散列表的裝填程度越高,反之愈小。對於使用鏈表法的散列表來說,查找一個元素的平均時間是O(1+a),因此如果負載因子越大,對空間的利用更充分,然而後果是查找效率的降低;如果負載因子太小,那麼散列表的數據將過於稀疏,對空間造成嚴重浪費。
- HashMap的實現中,通過threshold字段來判斷HashMap的最大容量:
threshold = (int)(capacity * loadFactor)
結合負載因子的定義公式可知,threshold就是在此loadFactor和capacity對應下允許的最大元素數目,超過這個數目就重新resize,以降低實際的負載因子。默認的的負載因子0.75是對空間和時間效率的一個平衡選擇。當容量超出此最大容量時, resize後的HashMap容量是原來容量的兩倍
6. HashMap的Fail-Fast機制
- java.util.HashMap不是線程安全的,因此如果在使用迭代器的過程中有其他線程修改了map,那麼將拋出ConcurrentModificationException,這就是所謂fail-fast策略。
- 這一策略在源碼中的實現是通過modCount域,modCount顧名思義就是修改次數,對HashMap內容的修改都將增加這個值,那麼在迭代器初始化過程中會將這個值賦給迭代器的expectedModCount。
HashIterator() {
expectedModCount = modCount;
if (size > 0) { // advance to first entry
Entry[] t = table;
while (index < t.length && (next = t[index++]) == null)
;
}
}
- 在迭代過程中,判斷modCount跟expectedModCount是否相等,如果不相等就表示已經有其他線程修改了Map (注意到modCount聲明爲volatile,保證線程之間修改的可見性。
final Entry<K,V> nextEntry() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
- 一般來說,存在非同步的併發修改時,快速失敗迭代器盡最大努力拋出 ConcurrentModificationException,迭代器的快速失敗行爲應該僅用於檢測程序錯誤。
7. HashMap和Hashtable
- HashMap不是線程安全的;HashTable是線程安全的,其線程安全是通過Sychronize實現。
- 由於上述原因,HashMap效率高於HashTable
- HashMap的鍵可以爲null,HashTable不可以
- 多線程環境下,通常也不是用HashTable,因爲效率低。HashMap配合Collections工具類使用實現線程安全。同時還有ConcurrentHashMap可以選擇,該類的線程安全是通過Lock的方式實現的,所以效率高於Hashtable。