Java基礎(三)HashMap原始碼剖析
關於HashMap,在網上看到了不少的好文章,萬花叢中過的過程中,我自己卻有了很大的感慨。
關於HashMap很多好的文章介紹,都是關注於HashMap的一個點,進行展開介紹。簡簡單單幾張圖,幾行文字介紹,就給你豁然開朗的感覺。
絕大多數的好文章,都是在圍繞著HashMap資料結構、儲存方式,在進行講解性介紹。
這些好文章,我看完之後,不免會產生很多的疑惑,本文就圍繞解決我的疑惑展開。
這裡順便說一下那麼想寫好HashMap,卻看上去不太好懂的文章,說實話,那些文章,其實我結合原始碼看,也給我很大收貨,但是為什麼說他文章寫的不太好呢?我想,是因為那些作者太想通過一篇文章,把HashMap介紹明白了。全面展開,反而處處得不到深入。
建議:想學習什麼深入原始碼、自己研究框架這種類似的東西,你一定要先老老實實先把原始碼跟一遍,然後再去看網上所謂的好文章和不太好的文章,不然,你就是在浪費時間。
HashMap的資料結構
HashMap是陣列、連結串列和雜湊表共同實現對資料的儲存,但是陣列和連結串列基本上是兩個極端。
陣列
陣列儲存區間是連續的,佔用記憶體記憶體較大。故空間複雜度很大,時間複雜度很小。陣列的特點:定址容易,插入和刪除困難;
連結串列
連結串列儲存區間離散,佔用記憶體比較寬鬆,故空間複雜度很小,但時間複雜度很大。連結串列特點:定址困難,插入和刪除容易。
資料結構中有陣列和連結串列來實現對資料的儲存,但這兩者基本上是兩個極端。
而HashMap則通過雜湊表,綜合陣列和連結串列兩者之所長,實現資料儲存。
我們在向HashMap中存取值的時候,我們會這樣做:
HashMap<String,String> map = new HashMap<String,String>();
//向map中存入一個值
map.put("A","123");
//根據key,取出map中值
map.get("A");
那在HashMap內部是怎麼實現的呢?偽程式碼表示的話,類似於下面:
// 儲存時: int hash = key.hashCode(); int index = hash % Entry[].length; Entry[index] = value; // 取值時: int hash = key.hashCode(); int index = hash % Entry[].length; return Entry[index];
但是,HashMap在原始碼中的實現,比上面的虛擬碼更加複雜。儲存時,得到key的hashCode值後,還會對key進行加工處理:
如果key是String,則使用stringHash32直接對key做hash;如果key不是String,則對key的hashCode,進行復雜的位運算,使得二進位制數中1的分佈更加均勻。這樣做的好處,就是儘量做到hash值不重複。
因為hash不重複的資料,在定址時,能夠通過key的hash值,一次性找到,而如果hash多次重複,則資料會以連結串列的形式儲存,根據key獲取值時,只能遍歷查詢效率極低的連結串列(不懂的話,先略過,一會兒再回頭來看)。
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();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
put
上面已經提到了連結串列,那HashMap內部是如何利用連結串列儲存的呢?
HashMap內部採用了Entry物件。Entry是一個靜態內部類。其屬性有:key、value、next和hash四個屬性。從屬性key、value我們就能很明顯的看出來Entry就是HashMap鍵值對實現的一個基礎bean,HashMap的基礎是一個線性陣列,這個陣列就是Entry[],Map裡面的內容都儲存在Entry[]裡面。
Entry的資料結構如下(Entry中提供的內部類方法忽略):
key就是鍵值對中的key;
value就是鍵值對中的value;
next為指標,當使用雜湊演算法,算出來的key重複,但key不重複時,則使用新的entry物件資料,指向舊的資料;
hash則是根據雜湊演算法和key,算出來的hash值。
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;
next = n;
key = k;
hash = h;
}
..........
}
下面直接分析原始碼吧。put 的原始碼如下:
//HashMap的put方法
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
//如果key是null值,單獨處理(其實處理方式,與非null完全類似)
if (key == null)
return putForNullKey(value);
//key不為null值,則對key做hash演算法(實際是對key的hashCode做hash演算法),根據key,得到hashCode值
int hash = hash(key);
//根據hashCode值,以及map中不衝突的值的數量,得到key所在的位置 AAA
int i = indexFor(hash, table.length);
//如果位置AAA上有值,則對Entry物件進行遍歷
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//判斷新增的key是否重複:如果entry位置上的key的hash值與新增的key的hash值相同,則新值替換久值,返回久值
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
//如果位置AAA上沒有值,則新增新的值(即沒有衝突的情況)
modCount++;
addEntry(hash, key, value, i);
return null;
}
請先不要考慮key為null的情況,我們根據上面提到的hash演算法,根據鍵值對的key,算出它對應的hash值,然後根據這個hash值 與 已經儲存hash不衝突資料的長度,確定值的位置或大概位置。
get
明白了put,則get就比較簡單了,key不為null時,直接根據getEntry方法和key,獲取Entry物件。
public V get(Object key) { //如果key為null,則按照null規則處理
if (key == null)
return getForNullKey();
//key!=null;呼叫getEntry方法
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
那getEntry是如何根據key,獲取其對應的value的呢?
比較簡單,還是根據key,拿到hash值,根據key的hash值與hash值不衝突的個數,獲取key的位置,拿到Entry陣列。
拿到Entry陣列之後,必須保證兩個條件,才能拿到key對應的value值:一、entry中的hash值與key的hash值必須相等;二、key與entry中的key的值相等。
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
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;
}
總結
總結起來,一共有以下幾點,需要你注意:
1、本文沒有考慮key為null的情況,HashMap的實現,是可以允許key為null的,大家自己可以看看原始碼,統一的方式,很簡單;
2、key雜湊值的確定。不同版本的JDK中,雜湊演算法可能不盡相同,但是它們的目的只有一個:就是要減少衝突。(為什麼減少衝突,我在文章最前面就提過,key的hash衝突的資料,需要以連結串列的形式,儲存為Entry物件陣列,衝突時,就要遍歷該查詢效率低下的連結串列);
3、如果key的hash衝突了,那麼如何進行儲存,如何確保根據key取value時能夠正確取到?這是大家容易迷糊地方,也是我之前比較迷糊地方。確定value 的值,必須根據如下兩個條件:一、entry中的hash值與key的hash值必須相等;二、key與entry中的key的值相等。這樣做,是為了排除key相同的情況。
例如put時,我們要確定entry陣列中,entry.hash值與value的hash值相等,並且,entry.key與key相等,則說明這是put進來了key相同的值,HashMap的實現是新值覆蓋原先的值;
get時,則同樣是判斷entry.hash值與key的hash值相等,並且,entry.key與key相等,來唯一確定value的值。確定唯一值的過程中,如果key的hash值有衝突,則要遍歷連結串列來實現唯一確定。
4、Entry陣列位置的確定。根據如下函式確定:h是key的hash值;length是Entry陣列的個數。
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);
}
比較好的幾篇文章推薦: