HashMap底層實現原理 擴容機制
實現原理:
HashMap本質是一個一定長度的陣列,陣列中存放的是連結串列。
它是一個Entry型別的陣列,Entry的原始碼:
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
final int hash;
Entry<K,V> next;
}
其中存放了Key,Value,hash值,還有指向下一個元素的引用。
當向HashMap中put(key,value)時,會首先通過hash演算法計算出存放到陣列中的位置,比如位置索引為i,將其放入到Entry[i]中,如果這個位置上面已經有元素了,那麼就將新加入的元素放在連結串列的頭上,最先加入的元素在連結串列尾。比如,第一個鍵值對A進來,通過計算其key的hash得到的index=0,記做:Entry[0] = A。一會後又進來一個鍵值對B,通過計算其index也等於0,現在怎麼辦?HashMap會這樣做:B.next = A,Entry[0] = B,如果又進來C,index也等於0,那麼C.next = B,Entry[0] = C;這樣我們發現index=0的地方其實存取了A,B,C三個鍵值對,他們通過next這個屬性連結在一起,也就是說陣列中儲存的是最後插入的元素。
HashMap的get(key)方法是:首先計算key的hashcode,找到陣列中對應位置的某一元素,然後通過key的equals方法在對應位置的連結串列中找到需要的元素。從這裡我們可以想象得到,如果每個位置上的連結串列只有一個元素,那麼hashmap的get效率將是最高的。所以我們需要讓這個hash演算法儘可能的將元素平均的放在陣列中每個位置上。
擴容機制:
當HashMap中的元素越來越多的時候,hash衝突的機率也就越來越高,因為陣列的長度是固定的。所以為了提高查詢的效率,就要對HashMap的陣列進行擴容。
HashMap的容量由一下幾個值決定:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // HashMap初始容量大小(16) static final int MAXIMUM_CAPACITY = 1 << 30; // HashMap最大容量 transient int size; // The number of key-value mappings contained in this map static final float DEFAULT_LOAD_FACTOR = 0.75f; // 負載因子 HashMap的容量size乘以負載因子[預設0.75] = threshold; // threshold即為開始擴容的臨界值 transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE; // HashMap的基本構成Entry陣列
當HashMap中的元素個數超過陣列大小(陣列總大小length,不是陣列中個數size)*loadFactor時,就會進行陣列擴容,loadFactor的預設值為0.75,這是一個折中的取值。也就是說,預設情況下,陣列大小為16,那麼當HashMap中元素個數超過16*0.75=12(這個值就是程式碼中的threshold值,也叫做臨界值)的時候,就把陣列的大小擴充套件為 2*16=32,即擴大一倍,然後重新計算每個元素在陣列中的位置。
0.75這個值成為負載因子,那麼為什麼負載因子為0.75呢?這是通過大量實驗統計得出來的,如果過小,比如0.5,那麼當存放的元素超過一半時就進行擴容,會造成資源的浪費;如果過大,比如1,那麼當元素滿的時候才進行擴容,會使get,put操作的碰撞機率增加。
HashMap中擴容是呼叫resize()方法,方法原始碼:
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
//如果當前的陣列長度已經達到最大值,則不在進行調整
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
//根據傳入引數的長度定義新的陣列
Entry[] newTable = new Entry[newCapacity];
//按照新的規則,將舊陣列中的元素轉移到新陣列中
transfer(newTable);
table = newTable;
//更新臨界值
threshold = (int)(newCapacity * loadFactor);
}
//舊陣列中元素往新陣列中遷移
void transfer(Entry[] newTable) {
//舊陣列
Entry[] src = table;
//新陣列長度
int newCapacity = newTable.length;
//遍歷舊陣列
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null;
do {
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);//放在新陣列中的index位置
e.next = newTable[i];//實現連結串列結構,新加入的放在鏈頭,之前的的資料放在鏈尾
newTable[i] = e;
e = next;
} while (e != null);
}
}
}
可以看到HashMap不是無限擴容的,當達到了實現預定的MAXIMUM_CAPACITY,就不再進行擴容。
Hashmap為什麼大小是2的冪次?
因為在計算元素該存放的位置的時候,用到的演算法是將元素的hashcode與當前map長度-1進行與運算。原始碼:
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);
}
如果map長度為2的冪次,那長度-1的二進位制一定為11111...這種形式,進行與運算就看元素的hashcode,但是如果map的長度不是2的冪次,比如為15,那長度-1就是14,二進位制為1110,無論與誰相與最後一位一定是0,0001,0011,0101,1001,1011,0111,1101這幾個位置就永遠都不能存放元素了,空間浪費相當大。也增加了新增元素是發生碰撞的機會。減慢了查詢效率。所以Hashmap的大小是2的冪次。
get方法實現
Hashmap get一個元素是,是計算出key的hashcode找到對應的entry,這個時間複雜度為O(1),然後通過對entry中存放的元素key進行equal比較,找出元素,這個的時間複雜度為O(m),m為entry的長度。