Java裡各種基礎容器的實現都是連結串列外加引用的形式。So,hashmap也不例外。
那小考慮下:hashmap是怎麼產生的呢?
常用的兩種資料結構陣列和連結串列各有優劣,陣列定址容易,插入和刪除困難;而連結串列定址困難,插入刪除容易,那我們能不能綜合兩者的特性呢?那就是雜湊表啦。hashmap是java中對於雜湊表的無鎖實現。
首先,說句題外話:Hashtable是HashMap的難弟兒,hashtable是hashmap的執行緒安全版本,它的實現和HashMap實現基本一致,除了它不能包含null值的key和value,並且它在計算hash值和陣列索引值的方式要稍微簡單一些。對於執行緒安全的實現,Hashtable簡單的將所有操作都標記成synchronized,即對當前例項的鎖,這樣容易引起一些效能問題,所以目前一般使用效能更好的ConcurrentHashMap。另外,對於HashMap是可以解決同步問題的,通過呼叫Map Collections.synchronizedMap(Map m),當然與可以自己在使用地方加鎖。
下面進入正題:
雜湊表有多種實現方法,java中實現採用的是拉鍊法,我們可以當成“連結串列的陣列”,如下圖所示:
先建立一個數組,在陣列中存放是是連結串列的頭節點。Java的HashMap裡面實現了一個靜態內部類Entry,其重要的屬性有Key Value Entry(next)。而陣列中儲存的就是Entry。
我們先來看hashmap建構函式的程式碼。如下:
this.loadFactor = DEFAULT_LOAD_FACTOR; //裝填因子,擴容的時候使用
threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR); //預設的容量閾值
table = new Entry[DEFAULT_INITIAL_CAPACITY];//構造一個以Entry為物件的陣列
init(); //空方法
由以上程式碼可以看出,初始化hashmap的過程只是初始化了一個Entry型別陣列,Entry就是剛才說的Java實現的內部類,我們來看下Entry的程式碼?
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
final int hash;
.......
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
} public final boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry e = (Map.Entry)o;
Object k1 = getKey();
Object k2 = e.getKey();
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
Object v1 = getValue();
Object v2 = e.getValue();
if (v1 == v2 || (v1 != null && v1.equals(v2)))
return true;
}
return false;
} public final int hashCode() {
return (key==null ? 0 : key.hashCode()) ^
(value==null ? 0 : value.hashCode());
}/**
* This method is invoked whenever the value in an entry is
* overwritten by an invocation of put(k,v) for a key k that's already
* in the HashMap.
*/
void recordAccess(HashMap<K,V> m) {
}
/**
* This method is invoked whenever the entry is
* removed from the table.
*/
void recordRemoval(HashMap<K,V> m) {
}
}
可以看出Entry是一個連結串列一樣結構,其中有next域用於指向下一個Entry。
既然是Entry陣列,那為什麼能線性插入刪除呢?那麼我們看一下往hashmap裡put之後都發生了什麼呢?來看程式碼。
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key); // 對key的雜湊在做一個一些移位異或的操作,目的是防止一些雞肋的key的雜湊函式
int i = indexFor(hash, table.length);//返回hash對應的陣列的下標。最簡單的實現肯定是用hash%length,但這裡不是這樣,這裡的處理很巧妙,下面還會再介紹。
//遍歷連結串列
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//如果key存在的話,就替換掉
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++; //修改次數,用於fail-fast策略
addEntry(hash, key, value, i); //新增entry到對應的陣列中,函式見下
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);//如果大小超過閾值的話,resize閾值。resize的過程原存在的Entry會重新計算索引值,並且Entry鏈的順序也會發生顛倒(如果還在同一個鏈中的話);而該新新增的Entry的索引值也會重新計算。,消耗還是蠻大的,所以如果知道hashmap大小的話,最好還是給個初始值
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);//插入到頭部,e表示Entry.next
size++;
}
}
return null;
}
上文說的利用key的二次hash值求對應的下標的時候,最常規的想法就是用hash%length,但是在jdk裡,沒這麼實現。而是
static int indexFor(int h, int length) {
return h & (length-1);
}
這個方法非常巧妙,它通過 h & (table.length -1) 來得到該物件的儲存位,而HashMap底層陣列的長度總是 2 的 n 次方(可以看初始化的程式碼,擴容的時候也是擴容到2的那次方)。這看上去很簡單,其實比較有玄機的,而當陣列長度為16時,即為2的n次方時,2的n次方得到的二進位制數的每個位上的值都為1,這使得在低位上&時,得到的和原hash的低位相同,加之hash(int h)方法對key的hashCode的進一步優化,加入了複雜的異或和位計算,就使得只有相同的hash值的兩個值才會被放到陣列中的同一個位置上形成連結串列。這樣效率提高了,而且衝突的可能性也減少了。
知道put操作作了什麼,get的話就不難了。
就先寫到這。