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

Java equals 與 hashCode 契約深度解析:HashMap/HashSet 正確運作的核心基石

     編輯:Java 相關知識

為什麼需要這對契約

哈希容器(HashMap、HashSet、Hashtable)依賴 hashCode() 快速定位桶位置,再用 equals() 精確比較。若契約被破壞,將導致:

  • 重複元素無法識別(Set 失效)
  • Key 查找失敗(Map 取值為 null)
  • 內存洩漏(無法移除元素)
  • 性能退化(哈希碰撞過多轉鏈表/紅黑樹)

核心契約:三條鐵律

  1. 自反性:x.equals(x) == true
  2. 對稱性:x.equals(y) == y.equals(x)
  3. 傳遞性:x.equals(y) && y.equals(z) ⇒ x.equals(z)
  4. 一致性:多次調用結果不變(前提字段未修改)
  5. 非空性:x.equals(null) == false
  6. 哈希一致性:equals 相等 ⇒ hashCode 必相等(逆命題不成立)

標準實現模板 (IDE 自動生成/手寫規範)

public class Person {
    private Long id;
    private String name;
    private LocalDate birthday;
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;                    // 1. 同引用快速返回
        if (o == null || getClass() != o.getClass()) return false; // 2. null/類型檢查
        Person person = (Person) o;
        return Objects.equals(id, person.id) &&        // 3. 逐字段比較
               Objects.equals(name, person.name) &&
               Objects.equals(birthday, person.birthday);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(id, name, birthday);       // JDK 7+ 推薦
        // 手寫版:31 * (31 * id.hashCode() + name.hashCode()) + birthday.hashCode()
    }
}

HashMap 存取流程與契約關鍵點

// put 流程簡化
int hash = hash(key.hashCode()); // 擾動函數
int index = (n - 1) & hash;      // 定位桶

Node e = table[index];
if (e == null) {
    newNode(hash, key, value, null); // 直接插入
} else {
    // 遍歷鏈表/樹
    for (Node cur = e; cur != null; cur = cur.next) {
        if (cur.hash == hash && (cur.key == key || key.equals(cur.key))) {
            // 找到重複 key:hash 相等 AND equals 相等
            V old = cur.value;
            cur.value = value;
            return old;
        }
    }
    // 未找到,鏈表尾部新增
}

// get 流程
Node e = table[index];
for (Node cur = e; cur != null; cur = cur.next) {
    if (cur.hash == hash && (cur.key == key || key.equals(cur.key))) {
        return cur.value; // 必須 equals 相等才能命中
    }
}
return null;

關鍵洞察:hashCode() 決定搜索範圍(哪個桶),equals() 決定精確匹配。hashCode 不等直接跳過 equals,提升性能。

常見破壞契約場景與後果

錯誤代碼違反契約後果
只重寫 equals,不重寫 hashCodeequals 相等但 hashCode 不等Set 存入重複元素,Map 找不到已存 Key
hashCode 依賴可變字段對象修改後 hashCode 改變Map/Set 中元素「丟失」,無法 remove
equals 未檢查 getClass,用 instanceof破壞對稱性(子類重寫)Set 行為異常,違反集合規範
equals 比較非核心業務字段業務邏輯不一致去重/查找語義錯誤

String 作為不可變模式典范

// String 核心實現片段
public final class String {
    private final char value[]; // JDK 8 為 char[], JDK 9+ 為 byte[] + coder
    private int hash; // 默認 0 表示未計算
    
    public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            hash = h = isLatin1() ? StringLatin1.hashCode(value)
                                  : StringUTF16.hashCode(value);
        }
        return h;
    }
    
    public boolean equals(Object anObject) {
        if (this == anObject) return true;
        if (anObject instanceof String aString) { // 模式匹配
            // 比較 coder + value 數組內容
        }
        return false;
    }
}

優勢:不可變 ⇒ hashCode 緩存安全、線程安全、可作 Map Key/Set 元素、字符串常量池複用。

最佳實踐清單

  1. 必須成對重寫:重寫 equals 必重寫 hashCode,反之亦然
  2. 使用 Objects.equals/hash:空安全、代碼簡潔、JDK 內部優化
  3. 只用不可變字段:ID、創建時間、枚舉類型;排除集合、數組、可變對象
  4. 保持一致性:equals 用到的字段,hashCode 全部用上
  5. 不要在 equals 中做耗時操作:IO、網絡、複雜計算
  6. 考慮繼承影響:final 類最安全;非 final 類用 getClass() 而非 instanceof
  7. 測試驗證:單元測試覆蓋 equals/hashCode 契約(EqualsVerifier 库)
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved