public class Person {
private String name;
public Person(String name) { this.name = name; }
// 缺少 equals() 與 hashCode() 重寫
}
Map<Person, String> map = new HashMap<>();
map.put(new Person("hello"), "world");
map.get(new Person("hello")); // 返回 null!
原因:Person 未重寫 hashCode(),繼承 Object 實作 (基於記憶體位址)。兩個 new Person("hello") 是不同物件,哈希碼不同,落入不同桶位,導致查找失敗。
Thinking in Java)public class SimpleHashMap<K,V> extends AbstractMap<K,V> {
static final int SIZE = 997; // 質數減少衝突
LinkedList<MapEntry<K,V>>[] buckets = new LinkedList[SIZE];
public V put(K key, V value) {
int index = Math.abs(key.hashCode()) % SIZE;
if (buckets[index] == null) buckets[index] = new LinkedList<>();
// 遍歷鏈表尋找 key.equals() 相同節點,找到則替換,否則新增
}
public V get(Object key) {
int index = Math.abs(key.hashCode()) % SIZE;
if (buckets[index] == null) return null;
// 遍歷鏈表比對 key.equals()
}
}
核心結構:陣列 + 鏈表 (JDK 8+ 高頻桶位轉紅黑樹)。
transient Entry<K,V>[] table; // 必須是 2 的冪次
static class Entry<K,V> implements Map.Entry<K,V> {
final K key; V value; Entry<K,V> next; int hash; // 緩存 hash 值
}
table.length 為 2 的冪,索引計算用位運算 (n - 1) & hash 替代取模,效能更高hash() 方法對 key.hashCode() 二次攪拌 (高 16 位參與),減少劣質 hashCode 導致的衝突loadFactor * capacity (預設 0.75),擴容為 2 倍並重新計算索引public V get(Object key) {
if (key == null) return getForNullKey(); // null key 固定放 table[0]
Entry<K,V> entry = getEntry(key);
return entry == null ? null : entry.getValue();
}
final Entry<K,V> getEntry(Object key) {
if (size == 0) return null;
int hash = (key == null) ? 0 : hash(key); // 二次攪拌後的 hash
for (Entry<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;
}
判斷相同鍵的條件:hash 相等 且 (同一物件引用 或 equals() 返回 true)。
a.equals(b) 為 true,則 a.hashCode() == b.hashCode() 必須為 truea.hashCode() == b.hashCode(),a.equals(b) 可 true 可 false (允許衝突)equals() 必須滿足:自反性、對稱性、傳遞性、一致性、非空性public class Person {
private String name;
private int age;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age && Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age); // JDK 7+ 工具方法
}
}
| 陷阱 | 後果 | 解決方案 |
|---|---|---|
| 只重寫 equals 不重寫 hashCode | 邏輯相等物件落入不同桶,查找失敗 | 成對重寫,或使用 @EqualsAndHashCode (Lombok) |
| Key 為可變物件且修改了關鍵欄位 | 資料「丟失」(實際在舊桶位,新桶位找不到) | Key 使用不可變類;或修改前 remove、修改後再 put |
hashCode 返回常數 (如 return 1) | 所有元素擠入同一桶,退化為鏈表 O(n) | 合理分散 hash,結合多欄位 |
| 使用陣列/集合作 Key 且未正確實作 | 預設 identity hashCode,內容相同但引用不同視為不同鍵 | 轉為不可變封裝類,或用序列化字串作 Key |
treeifyBin()/untreeify() 中HashMap 以 hashCode() 決定桶位,equals() 解決衝突。自定義 Key 必須正確重寫兩者,且保證放入後不修改關鍵欄位。理解底層機制有助於診斷「明明放進去了卻取不出來」等疑難雜症,也是設計高效緩存鍵、分佈式一致性哈希的理論基礎。