首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

HashMap 核心原理深度剖析:哈希索引、鍵變更陷阱與自定義 Key 最佳實踐

     編輯:Java 相關知識

經典面試題:Key 修改屬性後能否取值?

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") 是不同物件,哈希碼不同,落入不同桶位,導致查找失敗。

HashMap 底層資料結構

簡化版實作原理 (參考 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+ 高頻桶位轉紅黑樹)。

JDK 實際實作關鍵點

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 倍並重新計算索引

get() 核心流程源碼解析

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)。

自定義 Key 的正確重寫規範

equals() 與 hashCode() 契約

  1. 若 a.equals(b) 為 true,則 a.hashCode() == b.hashCode() 必須為 true
  2. 若 a.hashCode() == b.hashCode(),a.equals(b) 可 true 可 false (允許衝突)
  3. equals() 必須滿足:自反性、對稱性、傳遞性、一致性、非空性

標準實作範本 (IDE 自動生成)

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 計算
  • 不可變物件最適合作 Key:String、Integer、Long 等包裝類、自定義不可變類
  • 若 Key 物件放入 Map 後修改了參與 hashCode 的欄位,將導致無法找回 (哈希桶位已改變)

常見陷阱與最佳實踐

陷阱後果解決方案
只重寫 equals 不重寫 hashCode邏輯相等物件落入不同桶,查找失敗成對重寫,或使用 @EqualsAndHashCode (Lombok)
Key 為可變物件且修改了關鍵欄位資料「丟失」(實際在舊桶位,新桶位找不到)Key 使用不可變類;或修改前 remove、修改後再 put
hashCode 返回常數 (如 return 1)所有元素擠入同一桶,退化為鏈表 O(n)合理分散 hash,結合多欄位
使用陣列/集合作 Key 且未正確實作預設 identity hashCode,內容相同但引用不同視為不同鍵轉為不可變封裝類,或用序列化字串作 Key

JDK 8+ 優化:紅黑樹化

  • 當桶位鏈表長度 ≥ 8 (且陣列長度 ≥ 64) 時,轉為紅黑樹
  • 查找複雜度從 O(n) 降為 O(log n)
  • 擴容/樹化/退化邏輯在 treeifyBin()/untreeify() 中

總結

HashMap 以 hashCode() 決定桶位,equals() 解決衝突。自定義 Key 必須正確重寫兩者,且保證放入後不修改關鍵欄位。理解底層機制有助於診斷「明明放進去了卻取不出來」等疑難雜症,也是設計高效緩存鍵、分佈式一致性哈希的理論基礎。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved