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

Java equals() 陷阱全解析:為什麼你的實現可能是錯的?

     編輯:Java 相關知識

震驚結論:幾乎所有 equals 實現都有缺陷

研究表明,Java 代碼庫中絕大多數 equals() 實現違反了等價關系約定(自反性、對稱性、傳遞性、一致性、非空性)。這不僅導致集合(HashSet、HashMap)行為異常,更是無數難以復現 Bug 的溫床。

四大經典陷阱

陷阱 1:方法簽名錯誤 —— 重載而非重寫

// 錯誤:參數類型為 Point,未重寫 Object.equals(Object)
public boolean equals(Point other) { ... }

// 正確:參數必須為 Object,並配合 instanceof
@Override public boolean equals(Object other) {
    if (other instanceof Point) {
        Point that = (Point) other;
        return this.x == that.x && this.y == that.y;
    }
    return false;
}

後果:集合調用 Object.equals(),導致 HashSet.contains() 失效。

陷阱 2:改 equals 不改 hashCode

HashSet 依賴 hashCode() 定桶位,再用 equals() 比較。若兩對象 equals() 返回 true 但 hashCode() 不同,會落入不同桶,導致 contains() 返回 false。

// 必須同時重寫
@Override public int hashCode() {
    return 31 * x + y; // 僅依賴 equals 使用的字段
}

陷阱 3:基於可變字段實現 equals/hashCode

若對象放入 HashSet 後修改了參與 equals/hashCode 的字段,對象會「消失」:contains() 返回 false,但迭代器能遍歷到它。

對策:僅基於不可變字段實現;或不重寫 equals/hashCode,改用單獨方法如 sameContent()。

陷阱 4:繼承下的對稱性與傳遞性失效

子類添加新字段並重寫 equals() 時,極易破壞約定:

  • 對稱性失效:parent.equals(child) == true 但 child.equals(parent) == false。
  • 傳遞性失效:red.equals(blue) == false,但兩者都等於同一 parent。

完美解決方案:canEqual 模式

引入 canEqual(Object other) 方法,顯式聲明「我願意與哪類對象比較」:

父類實現

public class Point {
    private final int x, y;
    // ... 構造器、getter ...

    @Override public boolean equals(Object other) {
        if (other instanceof Point) {
            Point that = (Point) other;
            return that.canEqual(this)  // 關鍵:雙向確認
                && this.x == that.x
                && this.y == that.y;
        }
        return false;
    }

    @Override public int hashCode() {
        return 31 * x + y;
    }

    // 默認:允許與所有 Point 實例比較(含未重寫 equals 的子類)
    public boolean canEqual(Object other) {
        return other instanceof Point;
    }
}

子類實現

public class ColoredPoint extends Point {
    private final Color color;
    // ... 構造器、getter ...

    @Override public boolean equals(Object other) {
        if (other instanceof ColoredPoint) {
            ColoredPoint that = (ColoredPoint) other;
            return that.canEqual(this)  // 關鍵:雙向確認
                && this.color.equals(that.color)
                && super.equals(that);
        }
        return false;
    }

    @Override public int hashCode() {
        return 31 * super.hashCode() + color.hashCode();
    }

    // 關鍵:限制只與 ColoredPoint 比較,拒絕與父類 Point 比較
    @Override public boolean canEqual(Object other) {
        return other instanceof ColoredPoint;
    }
}

驗證結果

Point p = new Point(1, 2);
ColoredPoint cp = new ColoredPoint(1, 2, Color.RED);
Point anon = new Point(1, 1) { @Override public int getY() { return 2; } };

Set<Point> set = new HashSet<>();
set.add(p);

set.contains(p);    // true
set.contains(cp);   // false —— 色彩點不等於普通點
set.contains(anon); // true  —— 匿名子類未重寫 canEqual,視為普通點

完美滿足所有等價關系約定,且保持了里氏替換原則的語義正確性。

關於 LSP 的澄清

有人質疑 canEqual 違反 LSP。實則不然:LSP 要求子類行為符合父類約定,而非行為完全一致。ColoredPoint 明確語義為「帶顏色的點不等於無色點」,這是領域邏輯決定,而非協違約定。

總結檢查清單

  1. 簽名正確:equals(Object) + @Override
  2. 同時重寫 hashCode(),僅用不可變字段
  3. 不可變字段實現;可變字段另寫比較方法
  4. 引入 canEqual() 處理繼承層次
  5. 單元測試驗證:自反性、對稱性、傳遞性、一致性、非空性

掌握 canEqual 模式,是寫出正確、可擴展 equals() 的關鍵。下次重寫時,請務必帶上它。

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