原文:https://mp.weixin.qq.com/s/QggmWkrgYrNtVkdSKYuRfg
HashMap不支持併發操作,源碼簡單。
數組+單向鏈表
綠色的實體是嵌套類 Entry 的實例,包含四個屬性:key、value、hash 值、用於單向鏈表的 next。
capacity:當前數組容量,始終保持 2^n,可以擴容,擴容後數組大小爲當前的 2 倍。
loadFactor:負載因子,默認爲 0.75。
threshold:擴容的閾值,等於 capacity * loadFactor
put過程
public V put(K key, V value) {
// 當插入第一個元素的時候,需要先初始化數組大小
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
// 如果 key 爲 null,感興趣的可以往裏看,最終會將這個 entry 放到 table[0] 中
if (key == null)
return putForNullKey(value);
// 1. 求 key 的 hash 值
int hash = hash(key);
// 2. 找到對應的數組下標(計算具體的數組位置)
int i = indexFor(hash, table.length);
// 3. 遍歷一下對應下標處的鏈表,看是否有重複的 key 已經存在,
// 如果有,直接覆蓋,put 方法返回舊值就結束了
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++;
// 4. 不存在重複的 key,將此 entry 添加到鏈表中,細節後面說
addEntry(hash, key, value, i);
return null;
}
初始化
在第一個元素插入 HashMap 的時候做一次數組的初始化,就是先確定初始的數組大小,並計算數組擴容的閾值。
private void inflateTable(int toSize) {
// 保證數組大小一定是 2 的 n 次方。
// 比如這樣初始化:new HashMap(20),那麼處理成初始數組大小是 32
int capacity = roundUpToPowerOf2(toSize);
// 計算擴容閾值:capacity * loadFactor
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
// 算是初始化數組吧
table = new Entry[capacity];
initHashSeedAsNeeded(capacity); //ignore
}
計算具體數組位置
使用 key 的 hash 值對數組長度進行取模:取 hash 值的低 n 位。如在數組長度爲 32 的時候,其實取的就是 key 的 hash 值的低 5 位,作爲它在數組中的下標位置。
static int indexFor(int hash, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return hash & (length-1);
}
添加節點到鏈表中
找到數組下標後,會先進行 key 判重,如果沒有重複,就準備將新值放入到鏈表的表頭。
主要邏輯就是先判斷是否需要擴容,需要的話先擴容,然後再將這個新的數據插入到擴容後的數組的相應位置處的鏈表的表頭。
void addEntry(int hash, K key, V value, int bucketIndex) {
// 如果當前 HashMap 大小已經達到了閾值,並且新值要插入的數組位置已經有元素了,那麼要擴容
if ((size >= threshold) && (null != table[bucketIndex])) {
// 擴容,後面會介紹一下
resize(2 * table.length);
// 擴容以後,重新計算 hash 值
hash = (null != key) ? hash(key) : 0;
// 重新計算擴容後的新的下標
bucketIndex = indexFor(hash, table.length);
}
// 往下看
createEntry(hash, key, value, bucketIndex);
}
// 這個很簡單,其實就是將新值放到鏈表的表頭,然後 size++
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
數組擴容
在插入新值的時候,如果當前的 size 已經達到了閾值,並且要插入的數組位置上已經有元素,那麼就會觸發擴容,擴容後,數組大小爲原來的 2 倍。(消耗性能!)
擴容就是用一個新的大數組替換原來的小數組,並將原來數組中的值遷移到新的數組中。
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, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
get 過程
-
根據 key 計算 hash 值。
-
找到相應的數組下標:hash & (length – 1)