HashMap儲存原理以及與hashcode、equals方法的關係
阿新 • • 發佈:2019-01-04
一、HashMap 儲存/讀取資料原理:
先放原始碼:
public class HashMap<K, V> extends AbstractMap<K, V> implements Cloneable, Serializable {
private static final int MINIMUM_CAPACITY = 4;
...
transient HashMapEntry<K, V>[] table;
...
private static final Entry[] EMPTY_TABLE
= new HashMapEntry[MINIMUM_CAPACITY >>> 1];
...
@Override public V put(K key, V value) {
if (key == null) {
return putValueForNullKey(value);
}
int hash = Collections.secondaryHash(key);
HashMapEntry<K, V>[] tab = table;
int index = hash & (tab.length - 1);
for (HashMapEntry<K, V> e = tab[index]; e != null; e = e.next) {
if (e.hash == hash && key.equals(e.key)) {
preModify(e);
V oldValue = e.value;
e.value = value;
return oldValue;
}
}
// No entry for (non-null) key is present; create one
modCount++;
if (size++ > threshold) {
tab = doubleCapacity();
index = hash & (tab.length - 1);
}
addNewEntry(key, value, hash, index);
return null;
}
...
public V get(Object key) {
if (key == null) {
HashMapEntry<K, V> e = entryForNullKey;
return e == null ? null : e.value;
}
int hash = Collections.secondaryHash(key);
HashMapEntry<K, V>[] tab = table;
for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)];
e != null; e = e.next) {
K eKey = e.key;
if (eKey == key || (e.hash == hash && key.equals(eKey))) {
return e.value;
}
}
return null;
}
...
}
HashMap中儲存資料是用一個數組來儲存的,也就是上面的table變數,其型別是HashMapEntry的陣列,
而HashMapEntry則是儲存鍵值對的資料結構,並且有本身型別的next變數,可以構成連結串列。
HashMap儲存資料時,首先根據key的hashcode值找到應該儲存在table陣列的下標位置,如果該位置之前沒有
儲存過值,也就是沒有發生碰撞,則儲存這個鍵值對物件到該位置中;如果發生了碰撞,也就是說有兩個物件的key
的hashcode值相等,那麼則需要通過key的equals方法判斷這兩個物件是否是同一個物件,如果是,那麼原本存
儲的舊值會被新值所替換;如果不是同一個物件,則把新的鍵值對物件儲存到舊的鍵值對物件next變數中,構成連結串列。
我們分析下put方法的實現:
1、if (key == null) {
return putValueForNullKey(value);
}
首先判斷是否為null,如果為null則特殊處理;
2、int hash = Collections.secondaryHash(key);
獲取Key的二級hash值,其中Collections.secondaryHash方法的實現就是把Key的hashcode值
做一定改變;
3、int index = hash & (tab.length - 1);
通過剛才計算的hash值來獲取該key應該存放在陣列的下標位置,也就是獲取該資料應該儲存在table數
組的哪個位置;
4、for (HashMapEntry<K, V> e = tab[index]; e != null; e = e.next) {
if (e.hash == hash && key.equals(e.key)) {
preModify(e);
V oldValue = e.value;
e.value = value;
return oldValue;
}
}
如果已經有該key存在了,則覆蓋這個key的值value。
注意這裡的判斷:因為只有兩個物件的hashcode值相等並且兩個物件用equals判斷返回true時,才
去覆蓋原有的值;
5、
if (size++ > threshold) {
tab = doubleCapacity();
index = hash & (tab.length - 1);
}
addNewEntry(key, value, hash, index);
如果該key不存在,或者發生碰撞的物件不是一個物件時,則需要把它儲存下來。首先如果儲存數量已經
大於陣列大小,則把陣列雙倍擴大。然後再把鍵值對儲存到陣列中。
注意這裡儲存的時候,如果陣列儲存位置原本就存在鍵值對,那麼則把新的鍵值對物件儲存到舊的鍵值對
物件next變數中,構成連結串列。
二、HashMap與hashcode、equals方法的關係
它們的關係從上面的原始碼都能略知一二,再說個實際情況。
假設你用自定義型別MyClass作為HashMap的Key,同時為了需求重寫了hashcode、equals方法(這個
很常見),那麼很有可能會影響HashMap的執行效率,例如:
1、重寫hashcode方法後,任何物件返回都是同一個hash值,那麼,每次儲存都會發生碰撞,所有物件都只會儲存
在HaspMap的一格中,HashMap就等於廢了;
2、重寫hashcode方法後,其返回值會隨屬性的變化而變化,這樣的話,因為HashMap是根據Key的hashcode
值儲存讀取的,如果同一個物件每次返回的hashcode都不一樣,則根本無法讀取你上次儲存的位置,也就是
HashMap會失效;
3、重寫equals方法後,只根據物件的某些屬性值相等與否來決定equals方法是否返回true。這樣的話,就有可
能兩個其實不是一個物件的,但是儲存到HashMap時,則被認為是一個物件,導致其值被覆蓋了;
還有很多很多要注意的情況,為了避免這些情況,我們需要注意一些地方:當你使用任何物件作為Key,那麼它必
須遵守了equals()和hashCode()方法的定義規則,並且當物件插入到Map中之後將不會再改變。