詳解HashMap
一、數據結構
首先我們來看看HashMap源碼中的“靜態類”Entry:
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
}
源碼過多,我們只展示其數據結構,是一個典型的鏈式數據結構,完全可以推測出HashMap解決Hash衝突的方式可能爲鏈地址法。
二、HashMap中的主要方法
1.put()方法
依舊根據源碼進行分析,對其數據結構進行更深入的驗證和分析:
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
//當table爲空時,傳入一個臨界值,構造一個新的table,table爲一個數組,存放多個Entry,該方法只允許最終數組大小爲2的冪
}
if (key == null)
return putForNullKey(value);
//允許加入一個key爲空的value
int hash = hash(key);
//獲取key的hash值,通過hash函數對key的HashCode進行處理得到的值
int i = indexFor(hash, table.length);
//根據Key的HashCode找到其在數組中的index:return h & (length-1);也就是使用數組長度求餘。
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
//循環遍歷數組中指定index中的Entry鏈表
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {//當key的HashCode值相同,且兩者equals爲true,開始覆蓋原來的key
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
//否則表示key爲空,插入新值
modCount++;
addEntry(hash, key, value, i);//加入一個新的Entry
return null;
}
2.get()方法:
源碼:public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
get方法相對簡單,如果key爲null則調用getForNullKey方法,否則getEntry,獲取指定entry。
3.總結
由以上源碼我們可以得出,HashMap解決衝突的方式是鏈地址法。Hash函數,爲key的HashCode值,與Hash表表長求餘,餘數爲插入表的index,若有衝突,則在該表的Entry後面鏈式插入。同時,可以根據源碼細節,看出HashMap允許key爲null。
三、addEntry()細節和HashMap的擴容
addEntry()源碼: //在table指定位置新增Entry, 這個方法很重要
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
//table容量不夠, 該擴容了(兩倍table),重點來了,下面將會詳細分析
resize(2 * table.length);
//計算hash, null爲0
hash = (null != key) ? hash(key) : 0;
//找出指定hash在table中的位置
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
//擴容方法 (newCapacity * loadFactor)
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
//如果之前的HashMap已經擴充打最大了,那麼就將臨界值threshold設置爲最大的int值
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
//根據新傳入的capacity創建新Entry數組,將table引用指向這個新創建的數組,此時即完成擴容
Entry[] newTable = new Entry[newCapacity];
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
//擴容公式在這兒(newCapacity * loadFactor)
//通過這個公式也可看出,loadFactor設置得越小,遇到hash衝突的機率就越小
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
//擴容之後,重新計算hash,然後再重新根據hash分配位置,
//由此可見,爲了保證效率,如果能指定合適的HashMap的容量,會更合適
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
在addEntry時,會判斷是否到了當前HashMap的容量臨界值,如果到了,則進行擴容:
擴容方式是直接將HashMap的數組長度翻倍,默認數組的長度爲16,負載因子爲0.75f,臨界值爲負載因子乘以數組長度。擴容時機爲key-value鍵值對也就是Entry的數量大於臨界值時,進行擴容,並在擴容後將原來的元素重寫排版。