大家在求職應聘java開發崗時,想必會經常被面試官問到HashMap是怎麼實現的問題,本文
通過jdk源碼來簡析HashMap的實現機制。
首先,HashMap繼承了AbstractMap,並實現了Map、Cloneable和Serializable接口,這裏不
作闡述。在Eclipse裏查看源碼,可得如下類結構截圖。
首先來看成員變量,注意到有一個Entry類型的數組table,這個其實就是真正用來存儲我們
的數據的地方,其中Entry是HashMap的一個靜態內部類,它包含一個next引用(源代碼如
下),其實它就是一個簡單的鏈表結構
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
final int hash;
/**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
到這裏,我們其實可以知道HashMap是怎麼回事了,它底層是利用數組(還記得那個table麼?)實現,數組每一個元素本身相當於一
個鏈表,我們的數據就存在這些個鏈表的域中!我們知道數組存取速度快,鏈表增刪方便,HashMap是在存取和增刪這兩方面取了一
個折中!結構圖如下
是不是很簡單?
我們再看前三個final變量,根據名字就知道,分別是HashMap的默認初始容量、最大容量
、默認負載因子。不錯,HashMap也是有最大容量的(2的30次方),當然平時我們都沒
機會讓它爆掉。爲了節省空間,HashMap一開始只是申請一個相對較小的空間(一般就是
默認的容量數值這裏是2的16次方)。對於負載因子,這裏有必要提一下,它和HashMap
的容量有關,比如負載因子爲0.75,表示12單位的空間只存儲12*0.75=8個單位,這個數
字8其實也就是threshold的意義!而size變量表示真個map存儲的鍵值對的個數,在後文
對put函數講解得時候會說到HashMap的擴容方式,它的判斷標準也是根據這個將size和
threshold變量進行大小比較。
再看它的構造函數,其中有四個構造函數,我們平時用的最多是第三個無參構造函數,
其源碼如下:
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
table = new Entry[DEFAULT_INITIAL_CAPACITY];
init();
}
這裏做一些基本的初始化工作,注意threshold等於什麼!這在之前已經說過。另外,
init()是個空方法,這裏不太清楚原因,有知道的網友,多多指教啊!其它三個構
造函數其實最終都是調用調用第一個構造函數,只是使用傳入的參數進行初始化。
接下來,看看兩個最具代表性的方法:put、get
對於添加鍵值對對象的put方法:
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode());
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(hash, key, value, i);
return null;
}
該方法,首先判斷key是否爲null,如果null則調用putForNullKey方法,查看源碼發現putForNullKey方法默認
將Null對應的value放在table[0]位置所在的entry鏈表上(因爲有的key會映射到該
位置),所以HashMap是支持null爲key的!然後,通過就是計算key對應的hash
值,hash和indexFor函數如下:
static int hash(int h) {
// 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);
}
/**
* Returns index for hash code h.
*/
static int indexFor(int h, int length) {
return h & (length-1);
}
沒啥特別要說的,大底知道是通過無符號位移操作再與table的長度進行與操作,且不提。在put函數的最後有一行
addEntry(hash, key, value, i);
也就是添加元素到對應的鏈表中,其源碼爲:
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
if (size++ >= threshold)
resize(2 * table.length);
}
看到其中的if 語句了麼? 當元素越來越多時,hashmap的擴容方式爲擴大一倍!再接着,通過k的hashcode和k值判斷是否衝突!如果衝突則將
value覆蓋,並返回覆蓋之前的value,當然,沒有衝突和發生衝突的key值爲
null時都返回null (0.0, 別暈)
接下來,看看get方法:
public V get(Object key) {
if (key == null)
return getForNullKey();
int hash = hash(key.hashCode());
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.equals(k)))
return e.value;
}
return null;
}
同put一樣,先判斷key值是否爲null。 然後通過計算hashcode形成對table的一個映射,找到所在的entry鏈表,
然後就是對該鏈表進行簡單的遍歷,蠻簡單,不多提了!
最後提一下,table數組是聲明爲transient的,這可以防止HashMap被序列化。
另外,HashMap還有很多內部iterator接口的實現類,其主要是對HashMap
提供不需要了解內部結構就可以進行訪問的目的,這個可以參考iterator設
計模式 。