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

Java 重寫 equals 方法完整指南:五大契約、三大反例與工程化最佳實踐

     編輯:Java 綜合教學

前言

equals() 是 Java 對象邏輯相等性的核心判斷依據,直接影響 HashMap、HashSet、ArrayList.contains() 等集合操作的正確性。本文系統梳理 equals 五大契約、三類經典違反案例、標準實現模板及 IDE 自動生成技巧,助你寫出健壯、符合規範的相等性判斷邏輯。

一、equals 五大契約(JLS / Effective Java)

契約數學定義代碼含義違反後果
自反性x = xx.equals(x) == true對象無法在 Set 中正常去重、作為 Map Key 查找失敗
對稱性x = y ⇒ y = xx.equals(y) == y.equals(x)集合操作行為不可預測,set.contains(x) 與 set.contains(y) 結果不一致
傳遞性x = y ∧ y = z ⇒ x = zx.equals(y) && y.equals(z) ⇒ x.equals(z)集合邏輯崩潰,如 ArrayList.indexOf 返回錯誤索引
一致性不變則等多次調用結果不變(對象未修改)可變對象作為 Key 導致 Map 查找失效
非空性x ≠ nullx.equals(null) == false拋 NPE 或邏輯錯誤

二、三大經典反例深度剖析

1. 破壞自反性:大小寫轉換陷阱

public class AntiReflexive {
    private String str;
    
    @Override
    public boolean equals(Object o) {
        if (!(o instanceof AntiReflexive)) return false;
        AntiReflexive other = (AntiReflexive) o;
        return this.str.equals(other.str.toLowerCase()); // ❌ 破壞自反性
    }
    
    public static void main(String[] args) {
        AntiReflexive a = new AntiReflexive();
        a.setStr("Hello");
        System.out.println(a.equals(a)); // false!因為 "Hello".equals("hello") = false
    }
}

修正:雙向統一轉換或不轉換 return this.str.equalsIgnoreCase(other.str);

2. 破壞對稱性:跨類型比較單向支持

public class CaseInsensitiveString {
    private String s;
    
    @Override
    public boolean equals(Object o) {
        if (o instanceof CaseInsensitiveString) 
            return s.equalsIgnoreCase(((CaseInsensitiveString)o).s);
        if (o instanceof String) // ❌ 單向支持 String,但 String.equals 不知情
            return s.equalsIgnoreCase((String)o);
        return false;
    }
}

// 測試
CaseInsensitiveString cis = new CaseInsensitiveString("abc");
String str = "abc";
System.out.println(cis.equals(str)); // true
System.out.println(str.equals(cis)); // false!String 不知道 CaseInsensitiveString 存在

鐵律:絕不在 equals 中支持與非本類型的比較,除非該類型是你控制的最終類(final class)。

3. 破壞傳遞性:繼承鏈中混用 instanceof

class Point {
    int x, y;
    @Override public boolean equals(Object o) {
        if (!(o instanceof Point)) return false;
        Point p = (Point) o;
        return x == p.x && y == p.y;
    }
}

class ColorPoint extends Point {
    Color color;
    @Override public boolean equals(Object o) {
        if (!(o instanceof Point)) return false;
        if (!(o instanceof ColorPoint)) 
            return o.equals(this); // ❌ 委託給 Point,破壞傳遞性
        ColorPoint cp = (ColorPoint) o;
        return super.equals(o) && cp.color == this.color;
    }
}

// 測試
Point p = new Point(1, 2);
ColorPoint cp1 = new ColorPoint(1, 2, Color.RED);
ColorPoint cp2 = new ColorPoint(1, 2, Color.BLUE);

System.out.println(p.equals(cp1)); // true (Point 只比坐標)
System.out.println(cp1.equals(p)); // true (ColorPoint 委託 Point)
System.out.println(cp1.equals(cp2)); // false (顏色不同)
// 違反傳遞性:p=cp1, p=cp2 但 cp1≠cp2

結論:可實例化類不可繼承並重寫 equals,或使用組合代替繼承,或僅在 final 類中重寫。

三、標準實現模板(Effective Java 概要版)

public final class PhoneNumber {
    private final short areaCode, prefix, lineNum;

    public PhoneNumber(int areaCode, int prefix, int lineNum) {
        this.areaCode = rangeCheck(areaCode, 999, "area code");
        this.prefix   = rangeCheck(prefix,   999, "prefix");
        this.lineNum  = rangeCheck(lineNum, 9999, "line num");
    }

    @Override
    public boolean equals(Object o) {
        // 1. 自反性快速路徑
        if (o == this) return true;
        
        // 2. 類型檢查(用 instanceof 支持子類,但本類 final 所以等同 getClass())
        if (!(o instanceof PhoneNumber)) return false;
        
        // 3. 強轉
        PhoneNumber pn = (PhoneNumber) o;
        
        // 4. 關鍵字段逐個比較(優先比較差異度大、計算廉價的字段)
        return pn.lineNum == lineNum 
            && pn.prefix == prefix 
            && pn.areaCode == areaCode;
    }

    @Override
    public int hashCode() {
        // 5. 必須重寫 hashCode!公式:31 * 前一結果 + 字段hash
        int result = Short.hashCode(areaCode);
        result = 31 * result + Short.hashCode(prefix);
        result = 31 * result + Short.hashCode(lineNum);
        return result;
    }
    
    // 輔助方法...
}

四、工程化最佳實踐清單

  1. 使用 IDE 自動生成:IntelliJ Alt+Insert → equals() and hashCode(),勾選所有非瞬態字段
  2. 優先比較基本類型/不可變對象:int、String、enum 比較快且無 NPE 風險
  3. 浮點數用 Float.compare/Double.compare:處理 NaN、-0.0 邊界
  4. 數組用 Arrays.equals/deepEquals:逐元素比較
  5. 忽略衍生/冗余字段:如面積 = 長 × 寬,只比長寬
  6. 類標記 final:防止繼承破壞傳遞性
  7. 不可變對象優勢:所有字段 final,構造器賦值,天然滿足一致性
  8. 單元測試覆蓋契約:使用 EqualsVerifier 庫自動驗證五大契約

五、常見誤區

誤區正確做法
參數類型寫成 equals(MyClass o)必須 equals(Object o),否則是重載非覆寫
忘記重寫 hashCodeequals 重寫必重寫 hashCode,否則 Map/Set 失效
用 getClass() != o.getClass() 替代 instanceoffinal 類可用;可繼承類須用 instanceof 保持多態性
直接比較浮點數 f1 == f2用 Float.compare(f1, f2) == 0
集合字段直接 list1 == list2用 list1.equals(list2) 比較內容

六、總結

  • equals 定義業務邏輯上的「同一性」,核心服務於集合框架
  • 嚴守自反、對稱、傳遞、一致、非空五大契約,否則集合行為未定義
  • 三大反例揭示:跨類型比較、繼承重寫、可變狀態是主要風險源
  • 標準模板:== 檢查 → instanceof → 強轉 → 關鍵字段比較 → hashCode 配套
  • 善用工具:IDE 生成 + EqualsVerifier 測試 + 不可變設計 = 高質量 equals
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved