Hashmap\LinkedHashMap的實現原理分析
雖然網上已有很多人寫關於HashMap原始碼分析的文章,但看完過一段時間後,又有點模糊了,覺得只有自己真正再將其履一遍,並真正把它能講清楚,自己才算真正掌握了。在讀本文之前如果你對以下幾個問題都瞭如指掌,此文可略過。
1. HashMap的資料結構是什麼?hash衝突是指什麼?
2. HashMap是怎麼解決hash衝突的,連結串列法是如何實現的?
3. 為什麼HashMap的容量必須是2的指數冪?
4. LinkedHashMap的實現與HashMap有哪些不同?LinkedHashMap是怎麼實現元素有序的?
5. 為什麼重寫equals方法必須重寫hashcode方法?
6. HashMap的put方法實現過程?
1. 資料結構
hashmap實際上是一個數組+連結串列的資料結構,hashmap底層是一個數組結構,陣列中每一項又是一個連結串列結構。連結串列是為了解決hash衝突。
/**
* 空表
*/
static final HashMapEntry<?,?>[] EMPTY_TABLE = {};
/**
* 內部是一個數組,陣列的每一項是一個連結串列,陣列的長度必須是2的指數冪,當需要時進行擴容
*/
transient HashMapEntry<K,V>[] table = (HashMapEntry<K,V>[]) EMPTY_TABLE;
/**
* 陣列的每一項是一個連結串列結構,通過next屬性指向下一項,連結串列的每一項的是一個map結構資料,包含key、value
*/
static class HashMapEntry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
HashMapEntry<K,V> next;
int hash;
}
2. hashmap的初始化
hashmap的建構函式中會對hashmap容量進行初始化,預設初始容量是16,裝載因為為0.75,即當容量達到12時,會對hashmap進行擴容,增大一部,變成32.我們也可以呼叫其建構函式,自己設定其容量大小。
//initialCapacity初始容量,預設16、loadFactor裝載因子,預設0.75
public HashMap(int initialCapacity, float loadFactor) {
}
3. hasmap存取實現
3.1 put操作
put操作實現的方式是:根據key計算出hash值,查詢在陣列中對應的位置,判斷該陣列中對應位置是否有值,有值再判斷是否有相同的key值,有則將新值替換舊值,並將舊值返回,儲存結束。該陣列在對應位置中沒有值,或是有值但沒有相同的key值,則新建一個Entry元素,並將其放到對應的陣列位置處。
先看下原始碼:
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
//判斷鍵是否為空,為空放到陣列頭部table[0]位置
return putForNullKey(value);
//步驟1:通過hash演算法計算出key的hash值
int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);
//步驟2:根據hash值找到其對應在hashmap陣列中位置
int i = indexFor(hash, table.length);
//步驟3:查詢hashmap的陣列判斷該位置是否有值,沒有直接略過for迴圈,有則進入for迴圈中查詢該位置對應的連結串列中元素的key值是否和當前要儲存的元素的key值相同
for (HashMapEntry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//如果hash值相同且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++;
//步驟4:如果當前元素的hash值對應陣列位置中連結串列不存在或是連結串列存在,但是連結串列中所有元素值的key與當前要儲存的元素key值不一樣的話,新建一個Entry儲存到hashmap中
addEntry(hash, key, value, i);
return null;
}
上述四個步驟並沒有體現出hash衝突的處理方法,hash衝突是指key值不一樣,但是計算出的hash值是一樣的。我們接著看addEntry方法:
void addEntry(int hash, K key, V value, int bucketIndex) {
//步驟1:檢查當前hashmap中有值的陣列是否達到極限值(預設初始容量16*0.75),若超過,會進行陣列擴容。重新計算出當前要存入元素hash值對應在新陣列中的位置
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? sun.misc.Hashing.singleWordWangJenkinsHash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
//步驟2:新建一個Entry物件存入hashmap中
createEntry(hash, key, value, bucketIndex);
}
我們再進入createEntry函式檢視hashmap是怎麼將新元素存入陣列中的。
void createEntry(int hash, K key, V value, int bucketIndex) {
//步驟1:取出當前陣列中的元素,是一個Entry連結串列結構資料
HashMapEntry<K,V> e = table[bucketIndex];
//步驟2:將當前元素加入到Entry中,將原值e和新值的key,value,hash傳進去
table[bucketIndex] = new HashMapEntry<>(hash, key, value, e);
size++;
}
在這裡還沒有體現出hashmap處理衝突的方式,我們接著看新建一個Entry元素方法。
HashMapEntry(int h, K k, V v, HashMapEntry<K,V> n) {
value = v;//新值value
next = n;//關鍵點,將舊值賦給新值的next,這樣就相當於把新值放在連結串列頭部,新元素的next指向原陣列連結串列中的值
key = k;
hash = h;
}
終於找到了hashmap處理衝突的程式碼,當發生hash衝突時,會將新元素放在連結串列頭部,並將next指向原連結串列中元素。打個比方, 第一個鍵值對A進來,通過計算其key的hash得到的index=2,記做:Entry[2] = A。一會後又進來一個鍵值對B,通過計算其index也等於2,HashMap會將B.next = A,Entry[2] = B,如果又進來C,index也等於2,那麼C.next = B,Entry[2] = C;這樣我們發現index=2的地方其實存取了A,B,C三個鍵值對,他們通過next這個屬性連結在一起。
3.2 根據hash值查詢在陣列中位置
static int indexFor(int h, int length) {
return h & (length-1);
}
按位取並,作用上相當於取模mod或者取餘%操作,但是利用二進位制操作更快。這裡就體現了為什麼陣列長度必須是2的指數冪,只有2的指數冪,比如16的二進位制表示為 10000,那麼length-1就是15,二進位制為01111,同理擴容後的陣列長度為32,二進位制表示為100000,length-1為31,二進位制表示為011111。而擴容後只有一位差異,也就是多出了最左位的1,這樣在通過 h&(length-1)的時候,只要h對應的最左邊的那一個差異位為0,就能保證得到的新的陣列索引和老陣列索引一致,大大減少了之前已經雜湊良好的老陣列的資料位置重新調換.
3.3 存放key為空值實現
key值為空的所有元素都放到陣列的頭部table[0]位置處,這裡會判斷陣列頭部table[0]位置處是否有值,有值,再判斷是否有相同的key值,有則替換,沒有則新建一個Entry,步驟跟上面類似
private V putForNullKey(V value) {
for (HashMapEntry<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++;
addEntry(0, null, value, 0);
return null;
}
4 get方法
get操作的原始碼如下:
public V get(Object key) {
//如果key值為null,則從陣列頭部中查詢
if (key == null)
return getForNullKey();
//根據元素的key值查找出對應的Entry
Entry<K,V> entry = getEntry(key);
//返回Entry對應的value值
return null == entry ? null : entry.getValue();
}
我們再看看getEntry()方法的實現
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
//步驟一:計算出key對應的hash值
int hash = (key == null) ? 0 : sun.misc.Hashing.singleWordWangJenkinsHash(key);
//步驟二:查詢hash值對應在陣列中是否存在值,存在則判斷對應Entry的key是否和要查詢元素的key值相同,相同則返回對應的Entry
for (HashMapEntry<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;
}
5. hashmap遍歷方法
1)採用Iterator遍歷
Iterator iter = map.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry entry = (Map.Entry) iter.next();
Object key = entry.getKey();
Object val = entry.getValue();
}
2)for each遍歷
for (Entry<String, String> entry : map.entrySet()) {
Object key = entry.getKey();
Object val = entry.getValue();
}
6. LinkedHashMap原理
LinkedHashMap是HashMap的子類,實現結構和HashMap類似,只是HashMap中的連結串列是單向連結串列,而LinkedHashMap是雙向連結串列,只是在在HashMap的基礎上加上了排序實現。
6.1 建構函式
LinkedHashMap的建構函式較HashMap多了一個排序引數accessOrder。
/**
* @param initialCapacity 初始容量,16
* @param loadFactor 裝載因子0.75
* @param accessOrder 排序方式標識,為true代表陣列元素按照訪問順序排序,為false代表按照插入順序排序,預設按插入順序排序
*/
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
6.2 排序方式實現
在LinkedHashMapEntry中重寫了recordAccess方法,在HashMap的HashMapEntry是個空方法。該方法判斷是否按訪問順序進行排序,如果是呼叫addBefore()將當前被訪問的元素移至連結串列頭部。如果不是按訪問順序排序,則連結串列中元素沒有變化。因為插入元素時,新插入的元素在HashMap中實現就是放到連結串列頭部的。
void recordAccess(HashMap<K,V> m) {
LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
if (lm.accessOrder) {
lm.modCount++;
remove();
addBefore(lm.header);
}
}
/**
* 將當前元素移至連結串列頭部
*/
private void addBefore(LinkedHashMapEntry<K,V> existingEntry) {
after = existingEntry;
before = existingEntry.before;
before.after = this;
after.before = this;
}
6.3 put實現
LinkedHashMap沒有對put方式進行重寫,但對addEntry()、createEntry()和LinkedHashMapEntry中的recordAccess()方法進行了重寫。因為在HashMap中put一個元素時,如果要儲存的元素的hash值和key值在當前連結串列中存在的話,在替換舊值後,就呼叫了recordAccess()方法。而在createEntry()方法中,LinkedHashMap的實現如下,添加了addBefore()方法呼叫。將當前新插入的元素放至連結串列頭部。
void createEntry(int hash, K key, V value, int bucketIndex) {
HashMapEntry<K,V> old = table[bucketIndex];
LinkedHashMapEntry<K,V> e = new LinkedHashMapEntry<>(hash, key, value, old);
table[bucketIndex] = e;
e.addBefore(header);//關鍵點,將當前元素放到連結串列頭部,從而實現最近訪問的元素都在連結串列頭部
size++;
}
6.3 get實現
LinkedHashMap中,在進行獲取元素時,也呼叫了recordAccess方法,將訪問元素移至連結串列頭部
public V get(Object key) {
LinkedHashMapEntry<K,V> e = (LinkedHashMapEntry<K,V>)getEntry(key);
if (e == null)
return null;
e.recordAccess(this);//關鍵點,如果是按訪問順序排序,會將當前訪問的元素移至連結串列頭部
return e.value;
}
7. LruCache中呼叫LinkedHashMap的實現
final LinkedHashMap<T, Y> cache = new LinkedHashMap<T, Y>(100, 0.75f, true);
通過設定accessOrder引數為true,就實現了按訪問順序排序。
8、為什麼object類的equals()重寫同時也要重寫 hashcode ()?
Object物件的equals(Object)方法,對於任何非空引用值x和y,當且僅當x和y引用同一個物件時,此方法才返回true。
當equals()方法被重寫時,通常必須重寫hashcode()方法,hash協定宣告相等物件必須具有相等的hashcode,如下:
1)當obj1.equals(obj2)為true時,obj1.hashCode()==obj2.hashCode()必須為true
2)當obj1.hashCode()==obj2.hashCode()為false時,obj1.equals(obj2)必須為false,若obj1.hashCode()==obj2.hashCode()為true時,obj1.equals(obj2)不一定為true.
若不重寫equals,比較的將是物件的引用是否指向同一塊記憶體地址,重寫後目的是為了比較兩個物件的value值是否相等。
Hashcode是用於雜湊資料快速存取,如利用HashSet\HashMap\Hashtable類來儲存資料時,都是先進行hashcode值判斷是否相同,不相同則沒必要再進行equals比較。
如果重寫了equeals()但沒重寫hashcode(),再new一個物件時,當原物件.equals(新物件)等於true時,兩者的hashcode卻不是一樣的,由此會得出兩個物件不相等的性況。在儲存時,也會發生兩個值一樣的物件,hashcode不一樣而儲存兩個,導致混淆。所以Object的equals()方法重寫同時也要重寫hashcode()方法。
經過自己擼一遍原始碼,自己再把它寫下來,對HashMap實現的理解又加深了一點映象。