Effective Java筆記之改寫equals的通用約定

改寫equals的通用約定

我們知道,在java的世界裏,所有的類都是Object的派生類,其實Java設計Object的緣由就是爲了擴展,它的所有非fina方法,包括equals、hashCode、toString和finalize都有明確的通用約定。任何一個改寫這些方法的時候,都得遵守這些約定。

改寫equals方法看起來非常簡單,但是許多改寫的方式會導致錯誤,而且後果很嚴重。要避免問題最簡單的方法就是不改寫equals方法,在這種情況下,每個實例只與自己相等。以下情況,不需要改寫equasl方法,

  1. 一個類的每個實例本質上都是唯一的。對於代表了活動而不是值的類,比如Thread類。
  2. 不關心一個類是否提供了邏輯相等的測試功能
  3. 超類已經改寫了equals,從超類繼承過來的行爲對於子類也是合適的。例如,大多數的Set都繼承了AbstractSet的equals實現,類似的還有List和Map。
  4. 一個類是私有的,並且確認它的equals方法永遠不會調用

那麼,什麼時候,應該改寫equals呢?當一個類有自己特定的“邏輯相等”概念,而超類並沒有改寫equals的情況下。這通常適用於“值類”情況下。比如,我們想比較兩個實例是否在值一樣,而不是他們是否指向同一個對象。

有一種值類對象是不需要改寫equals方法的,即類型安全枚舉類型。

比如,我們有以下代碼,

public class Point {
    private int x;
    private int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
    public static void main(String[] args) {
        Point p1 = new Point(2, 5);
        Point p2 = new Point(2, 5);
        System.out.println("p1 equals p2? " + p1.equals(p2));
    }
}

我們在Point中,並沒有重寫equals方法,當我們想要判斷p1和p2在值上是否相等的時候,我們判斷的是他們的地址是否一樣(也就是是否指向同一個對象),這是由於其調用了父類(Object)的equals方法,由於p1和p2是兩個實例,所以p1.equals(p2)的結果是false。顯然不是我們預期的結果,所以我們得重寫equals方法。

改寫前,就讓我們來了解一下equals的一些約定,
equals方法實現了等價關係;

  1. 自反性。對於任意的x,x.equals(x)一定爲true;
  2. 對稱性。對於任意的x,y,x.equals(y)和y.equals(x)的值是一樣的。
  3. 傳遞性。對於任意的x,y,z,若x.equals(y)爲true,y.equals(z)也爲true,則x.equals(z)也爲true。
  4. 一致性。對於任意的x,y,如果x和y沒有被修改,則多次調用x.equals(y)的結果是一樣的。
  5. 非空性。對於任意的x,x.equals(null)一定是false。

讓我們來逐條分析,
對於自反性,相比沒有啥可以說的。如果自己和自己都不想等的話,那一切不都亂套了麼?

對於對稱性,其意思是,在x,y是否相等這個問題上,是一致的,不能說x等於y,但y不等於x。
讓我們來看下面一個例子,

public class CaseInsensitiveString {

    private String s;
    public CaseInsensitiveString(String s) {
        if (s == null)
            throw new NullPointerException();
        this.s = s;
    }

    @Override
    public boolean equals(Object obj) {
        // TODO Auto-generated method stub
        if (obj instanceof CaseInsensitiveString)
            return s.equalsIgnoreCase(((CaseInsensitiveString) obj).s);
        if (obj instanceof String)
            return s.equalsIgnoreCase((String) obj);
        return false;
    }

    public static void main(String[] args) {
        CaseInsensitiveString cis = new CaseInsensitiveString("wangfabo");
        String s = "WangFaBo";
        System.out.println(cis.equals(s));
        System.out.println(s.equals(cis));
    }

}

上面的例子,我們可以分析cis.equals(s)的結果是true,但是s.equals(cis)結果確是false,這是因爲String的equals方法並沒有實現不區分大小寫。所以上個例子的equals違反了傳遞性規則,會給程序帶來錯誤。

對於傳遞性,它的意思是,如果一個對象等於第二個對象,第二個對象等於第三個對象,則第一個對象等於第三個對象,很好理解。考慮這樣的情形:一個程序員創建了一個子類,它爲超類增加了一個新的特徵(變量),那麼,新的特徵就會影響到equals的比較結果。
超類Point代碼,

public class Point {
    private int x;
    private int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

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

子類ColorPoint代碼,

public class ColorPoint extends Point {

    private Color color;

    public ColorPoint(int x, int y, Color color) {
        super(x, y);
        this.color = color;
    }
}

我們要給ColorPoint添加一個equals方法,如果,你完全不提供的話,那麼就會調用Point的equals方法,這樣就完全忽略了color變量,顯然不符合實際,假設我們這樣寫equals,

    public boolean equals(Object obj) {
        if (!(obj instanceof ColorPoint))
            return false;
        ColorPoint cp = (ColorPoint) obj;
        return super.equals(obj) && cp.color == color;
    }

看似很符合邏輯,當不是ColorPoint實例,就返回false,否則,比較x和y是否相等(調用super.equals方法)、和判斷Color是否一樣。

但是,考慮以下情況,

Point p = new Point(2, 5);
ColorPoint cp = new ColorPoint(2, 5, Color.red);

當我們比較p.equals(cp)時,會調用Point的equals方法,會將cp強制轉換爲Point類型,然後判斷x與y是否分別一致,結果是true;
但是當比較cp.equals(p)時,由於p並不是ColorPoint的實例,所以會返回false。
所以這個equals方法違背了第二個約定,對稱性。

我們可以修改代碼,讓equals方法接收Point類型,

    public boolean equals(Object obj) {
        // TODO Auto-generated method stub
        if (!(obj instanceof Point))
            return false;
        if (!(obj instanceof ColorPoint))
            return obj.equals(this);
        ColorPoint cp = (ColorPoint) obj;
        return super.equals(obj) && cp.color.equals(color);
    }

我們可以測試,p.equals(cp)和cp.equals(p)的結果都是true。但是這又帶來了另一個問題,考慮如下實例,

        Point p = new Point(2, 5);
        ColorPoint cp1 = new ColorPoint(2, 5, Color.red);
        ColorPoint cp2 = new ColorPoint(2, 5, Color.blue);
        System.out.println(p.equals(cp1));
        System.out.println(p.equals(cp2));
        System.out.println(cp1.equals(cp2));

我們可以得到p和p1相等,p和p2相等,由於傳遞性,我們可以得到p1和p2相等,但是結果卻是false(不相等)。這是,由於,在進行Point和ColorPoint比較的時候,會犧牲掉ColorPoint的屬性,所以只要ColorPoint的座標屬性和Point的座標屬性一樣,就判斷爲相等,這顯然是不對的。

怎麼解決呢?根據Java的一個設計原則:複合優於繼承,我們可以不讓ColorPoint繼承Point,而是使用Point,代碼如下,

public class ColorPoint {
    private Point point;
    private Color color;

    public ColorPoint(int x, int y, Color color) {
        point = new Point(x, y);
        this.color = color;
    }

    public Point asPoint(){
        return point;
    }

    public boolean equals(Object obj) {
        // TODO Auto-generated method stub
        if (!(obj instanceof ColorPoint))
            return false;
        ColorPoint cp = (ColorPoint) obj;
        return cp.point.equals(point) && cp.color.equals(color);
    }

    public static void main(String[] args) {
        Point p = new Point(2, 5);
        ColorPoint cp1 = new ColorPoint(2, 5, Color.red);
        ColorPoint cp2 = new ColorPoint(2, 5, Color.blue);
        System.out.println(p.equals(cp1));
        System.out.println(p.equals(cp2));
        System.out.println(cp1.equals(cp2));
    }
}

這樣,代碼就可以通過測試,符合傳遞性和對稱性。

對於一致性和非空性,就不在多談。較好理解。
本篇博客就寫到這裏,下篇介紹改寫hashCode的通用約定。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章