HashMap 是面試必考、生產高頻的核心集合。本文從哈希表基礎理論出發,逐行剖析 JDK 7 HashMap 源碼:put/get 流程、hash() 擾亂函數、indexFor() 定位、resize() 擴容遷移、長度為 2 的冪設計哲學,最後證明 equals/hashCode 契約的必要性。
key → hash → index → bucket → 遍歷鏈表,理想情況 O(1)transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE; // 主幹數組
static class Entry<K,V> implements Map.Entry<K,V> {
final K key; V value; Entry<K,V> next; int hash; // 單向鏈表節點
}
int size; // 實際 KV 對數
int threshold; // 擴容閾值 = capacity * loadFactor
final float loadFactor = 0.75f; // 負載因子
int modCount; // 快速失效計數器
put(K,V) 全流程public V put(K key, V value) {
if (table == EMPTY_TABLE) inflateTable(threshold); // 首次分配
if (key == null) return putForNullKey(value); // null 鍵固定 table[0]
int hash = hash(key); // 擾亂計算
int i = indexFor(hash, table.length); // 定位桶索引
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
if (e.hash == hash && (e.key == key || key.equals(e.key))) {
V old = e.value; e.value = value; e.recordAccess(this); return old; // 覆蓋
}
}
modCount++;
addEntry(hash, key, value, i); // 頭插法
return null;
}
hash(Object) — 擾亂函數final int hash(Object k) {
int h = hashSeed;
if (h != 0 && k instanceof String) return sun.misc.Hashing.stringHash32((String)k);
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12); // 高位混入低位
return h ^ (h >>> 7) ^ (h >>> 4);
}
目的:將 hashCode() 的高位信息混入低位,減少碰撞。
indexFor(int h, int length) — 定位索引static int indexFor(int h, int length) { return h & (length - 1); }
length 為 2 的冪 → length-1 低位全 1 → & 運算等價於取模,且保留低位差異,分佈更均勻。
inflateTable(int toSize) — 首次/擴容分配int capacity = roundUpToPowerOf2(toSize); // 向上取 2 的冪
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
resize() 與 transfer()void transfer(Entry[] newTable, boolean rehash) {
for (Entry<K,V> e : table) {
while (e != null) {
Entry<K,V> next = e.next;
if (rehash) e.hash = (e.key == null) ? 0 : hash(e.key);
int i = indexFor(e.hash, newTable.length);
e.next = newTable[i]; // 頭插法,順序反轉
newTable[i] = e;
e = next;
}
}
}
關鍵洞察:長度雙倍擴容 → length-1 只多一個高位 1 → 原索引高位為 0 的節點留在原桶,為 1 的遷移到新桶,最小化遷移開銷。
h & (length-1) 等價取模,位運算比除法快length-1 低位全 1,保證 hash 低位每一位變化都影響索引,分佈均勻get(Object) 與 equals/hashCode 契約public V get(Object key) {
if (key == null) return getForNullKey();
Entry<K,V> e = getEntry(key);
return e == null ? null : e.value;
}
final Entry<K,V> getEntry(Object key) {
int hash = (key == null) ? 0 : hash(key);
for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
if (e.hash == hash && (e.key == key || key.equals(e.key))) return e;
}
return null;
}
驗證 e.hash == hash 的必要性:若 key.equals() 返回 true 但 hashCode() 不同 (違反契約),不應匹配,否則會從錯誤桶取出錯誤值。
equals 忘寫 hashCodeclass Person { int id;
@Override public boolean equals(Object o) { return o instanceof Person && id == ((Person)o).id; }
// 缺失 hashCode()
}
Map<Person,String> map = new HashMap<>();
map.put(new Person(1), "喬峰");
map.get(new Person(1)); // 返回 null!因為 hashCode 不同,落入不同桶
鐵律:equals() 相等 → hashCode() 必須相等;hashCode() 相等 → equals() 不一定相等 (允許碰撞)。