源碼分析:HashMap
作為以key/value存儲方式的集合,HashMap可以說起到了極大的作用。因此關於HashMap,我們將著重使用比較大的篇幅。
接下來會用到的幾個常量static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
static final int MAXIMUM_CAPACITY = 1 << 30;
static final int MAXIMUM_CAPACITY = 1 << 30;
先簡單過一下,HashMap的思路
我們put的key/value會被封裝成一個叫做Entry的內部類。這個Entry由一個變量名為table數組管理。我們每次put會通過一系列的計算,計算一個table數組的index下標用於放Entry,如果出現hash沖突使用鏈表法解決。get時,可以理解是一個反向的put過程。
put(K key, V value)
1、初始化
if (table == EMPTY_TABLE) { //threshold變量在初始化的時候使用DEFAULT_INITIAL_CAPACITY(16)初始化 inflateTable(threshold); } private void inflateTable(int toSize) { //計算我們table數組應該有多大(初始化是16) int capacity = roundUpToPowerOf2(toSize); //重新給threshold賦值:數組容量*0.75 threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); //初始化數組 table = new Entry[capacity]; //根據註釋:應用於推遲初始化(暫時不做深究) initHashSeedAsNeeded(capacity); }
走到這一步,相當於我們的第一次put的初始化過程完成。那麽接著讓我們看下一步操作。
2、key為null
當然這一步的前面還有一個key為null的情況。因為實在是太直白,就不單獨展開,代碼如下:
if (key == null) return putForNullKey(value); private V putForNullKey(V value) { //在0位置上的Entry鏈上遍歷,如果已存在key為null的Entry,替換value,並返回老的value for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; //0位置如果沒有存在Entry,直接正常add我們這個key為null的Entry addEntry(0, null, value, 0); return null; }
這裏我們可以得到一個信息,key為null的Entry會放在table[0]的位置上。
3、index下標計算
走到這一步,我們所要做的就是先計算我們這個key/value應該放在table的那個位置。也就是說,在真正包裝成Entry之前,我們需要確定這個Entry的應該再哪個坑裏。
計算hash值的hash方法如下:
//這個方法,的確是沒有太怎麽看明白是怎麽計算hash的,怪自己數據結構逃課過多吧,待日後有機會再補上...
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
//註釋翻譯:該函數確保在每個數組位置上僅以恒定倍數不同的散列碼,具有有限數量的沖突(在默認加載因子下約為8)。
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
計算完hash之後,就是通過hash計算我們Entry應該在那個下標中:
//這倆個參數一個是上文計算的hash,一個是table的length
static int indexFor(int h, int length) {
return h & (length-1);
}
走到這一步我們Entry該放在哪個位置已經明確了,這裏有很多位運算...根據效果來看,這樣的計算方式保證了,Entry更少的沖突。
4、插入Entry
既然上訴2的過程已經確定了插入位置,那麽毫無疑問,我們該插入這個
Entry了。
1、重復key處理
既然插入,那麽勢必有可能遇到重復問題,比如說,我們插入同一個key。這裏就是一個比較常見的問題,Map可不可以使用重復key,或者Map怎樣處理重復key的問題。
//如果index有存在的Entry,很簡單for循環遍歷這條鏈
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))) {
//直接替換value,並且返回老的value
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
因此關於key重復的問題,我們就可以得到答案。Map的操作是替換舊的value並返回老的value。
2、擴容
上訴步驟我們處理的key重復的問題。那麽接下來,就是Map的擴容過程。這裏會用到一個變量threshold,我們知道初始化table之後,這個變量 = 數組長度*0.75。記住這個值,它就是擴容的閾值。
擴容的
void addEntry(int hash, K key, V value, int bucketIndex) {
//當前put進來的size>=threshold並且index下標不為空
if ((size >= threshold) && (null != table[bucketIndex])) {
//擴容2倍
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
//重新計算index
bucketIndex = indexFor(hash, table.length);
}
//不屬於擴容的範疇
createEntry(hash, key, value, bucketIndex);
}
3、創建並添加
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
首先獲取index下標下的Entry,我們明白這裏的e有可能為空。(而這裏沒有對null這種情況進行判斷,也就是說這裏為不為空都無所謂)
拿到e之後,進行new Entry()把e,以及hash,key,value傳了進來。
這裏我們看一個e,放在了哪裏?
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
//我們重復的Entry直接被放在了next上。
next = n;
key = k;
hash = h;
}
//省略部分內容
}
這裏說明一個什麽問題?那就是當hash沖突之後,我們的Entry是在鏈表的頭還是尾。根據代碼來看,很明顯是在鏈表的頭,這也說明了,為什麽e為null這種情況沒有做特別處理。
我們對put的分析就到此為止,既然分析了put,那麽接下來就是get。
get(Object key)
1、處理key為null的情況
if (key == null)
return getForNullKey();
private V getForNullKey() {
//如果當前size為0,可以就沒有對應的value
if (size == 0) {
return null;
}
//上文中我們分析到,put,key為null的value時,是放在table[0]上,那麽取的時候,肯定也是去table[0]上去取。
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}
關於key為null的情況,其實我們也能看出,比較簡單明了。接下來就是key不為null的情況。
2、key不為null
final Entry<K,V> getEntry(Object key) {
//判空
if (size == 0) {
return null;
}
//通過key計算一個hash值。這裏的key為null的判斷,其實多此一舉。
int hash = (key == null) ? 0 : hash(key);
//反向計算index,也就是對應這個key的Entry在數組的哪個下標中。
//因為我們put的時候知道,有可能index是會重復的。因此這裏使用了一個循環去遍歷這條鏈。如果hash相同,且key相同,那麽就是我們要找的value,返回即可。
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;
}
代碼思路比較的明確,其實就是一個反向的put過程。我們先通過key計算hash,然後計算對應Entry在數組中的index,然後遍歷對應鏈,找出匹配的value即可。
get方法我們可以看出,是比較簡單的。
尾聲
分析完put/get其實基本上HashMap就梳理完畢。
這裏我們進行一點總結:
- 1、put時,key可以為空。並且放在table[0]的這個位置
- 2、擴容策略是,size>=當前容量*0.75並且當前table[index]不為null。擴容大小為2倍。
- 3、JDK1.7的HashMap使用鏈表法解決沖突,並且新插入的Entry是在鏈表的表頭。
源碼分析:HashMap