更好的重寫equals方法

何時重寫Equals方法

若是類具備本身特有的「邏輯相等」概念,並且父類尚未重寫 equals 方法以實現指望的行爲時,就須要重寫 equals 方法。java

這樣作還可使得這個類的實例能夠被用做 Map 的 Key,或者 Set 的元素,使 Map 或 Set 表現出預期的行爲來。ide

重寫Equals的指望結果

在重寫 equals 方法的時候,若是知足瞭如下任何一個條件,就正是所指望的結果:測試

  1. 類的每一個實例本質上都是惟一的;
  2. 不關心類是否提供了 logical equality 的測試功能;
  3. 父類已經覆蓋了 equals 方法,從父類繼承過來的行爲對於子類也是合適的;
  4. 類是私有的或者是包級私有的,確保它的 equals 方法永遠不會被調用時須要重寫 equals 方法;

重寫Equals的原則

自反性(reflexive):對於任何非 null 的引用值 x,x.equals(x) 必須返回 true;flex

對稱性(symmetric):對於任何非 null 的引用值 x and y,當且僅當 y.equals(x) 返回 true 時,x.equals(y) 必須返回 true;this

傳遞性(consistent):對於任何非 null 的引用值 x y z,若是 x.equals(y) 返回 true,而且 y.equals(z) 返回 true,那麼 x.equals(z) 也必須返回true;spa

一致性(consistent):對於任何非 null 的引用值 x and y,只要equals的比較操做在對象中所用的信息沒有被修改,那麼屢次調用 x.equals(y) 都會一致地返回 true,或者一致地返回 false;.net

非空性(non nullity):對於任何非 null 的引用值 x,x.equals(null) 必須返回 false;code

自反性

通常不會違背這個原則,若是違背了,那麼出現的現象會是將該類的實例添加到集合後,調用 contains 方法查找這個實例,會獲得一個 false。對象

對稱性

對稱性簡單說是,任何兩個對象對於它們是否相等的問題都必須保持一致, x 等於 y 那麼 y 也要等於 x。舉個違反這個原則的例子:繼承

package test.ch01;

public class CaseInsensitiveString {

    private final String s;

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

    @Override
    public boolean equals(Object o) {
        if (o instanceof CaseInsensitiveString) {
            return s.equalsIgnoreCase(((CaseInsensitiveString) o).s);
        }
        if (o instanceof String) {
            return s.equalsIgnoreCase((String) o);
        }
        return false;
    }
}

在這個類中,equals 要作到與普通的字符串比較時不區分大小寫,其問題在於 String 類中的 equals 方法並不知道不區分大小寫,所以反過來比較並不成立,違反了對稱性。

package test.ch01;

public class Test {

    public static void main(String[] args) {

        CaseInsensitiveString cis = new CaseInsensitiveString("Hello");
        String s = "hello";

        System.out.println(cis.equals(s)); // true
        System.out.println(s.equals(cis)); // false
    }

}

解決這個問題,只須要把企圖與 String 互操做的代碼從 equals 方法中去掉就能夠了:

package test.ch01;

public class CaseInsensitiveString {

    private final String s;

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

    @Override
    public boolean equals(Object o) {
        return o instanceof CaseInsensitiveString && ((CaseInsensitiveString) o).s.equalsIgnoreCase(s);
    }
}
package test.ch01;

public class Test {

    public static void main(String[] args) {

        CaseInsensitiveString cis = new CaseInsensitiveString("Hello");
        String s = "hello";

        CaseInsensitiveString cis1 = new CaseInsensitiveString("hello");

        System.out.println(cis.equals(s)); // false
        System.out.println(s.equals(cis)); // false
        System.out.println(cis.equals(cis1)); // true
    }

}

傳遞性

傳遞性要求 x 等於 y,y 等於 z,那麼 x 也要等於 z。可是,此處有很是重要的一點,面嚮對象語言關於等價關係的一個基本問題:

沒法在擴展可實例化的類的同時,即增長新的值組件,同時又保留 equals 約定,除非願意放棄面向對象的抽象所帶來的優點。

考慮兩個類,它們是繼承關係:

package test.ch01;

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

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

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

 

package test.ch01;

public class ColorPoint extends Point {

    private final Color color;

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


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

}

若是 ColorPoint 類,不重寫 equals,而是直接從 Point 繼承過來,那麼 ColorPoint 與 Point 比較時會忽略掉顏色。

可是若是按照上面代碼,重寫 equals,那麼會違反一致性:

Point p1 = new Point(1, 2);
ColorPoint cp1 = new ColorPoint(1, 2, Color.RED);
System.out.println(p1.equals(cp1)); // true
System.out.println(cp1.equals(p1)); // false

總之面嚮對象語言的等價交換關係是一個問題。

里氏替換原則(Liskov substitution principle)認爲,一個類型的任何重要屬性也將適用於他的子類,所以爲該類型編寫的任何方法,在它的子類型上也應該一樣運行的很好。

雖然沒有一種使人滿意的辦法解決上面的問題,可是仍是有一個不錯的權宜之計:

package test.ch01;

public class ColorPoint {

    private final Point point;
    private final Color color;

    public ColorPoint(Point point, Color color) {
        this.point = point;
        this.color = color;
    }

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof Color)) {
            return false;
        }
        ColorPoint cp = (ColorPoint) o;
        return cp.point.equals(point) && cp.color.equals(color);
    }
}

 使用複合方式代替繼承,能夠解決上面的問題。

一致性

可變對象在不一樣的時候能夠與不一樣的對象相等,不可變的對象則不會這樣。若是認爲它應該是不可變的,就必須保證 equals 方法知足:相等的對象永遠相等,不想等的對象永遠不相等。

不管類是不是不可變的,都不要使 equals 方法依賴不可靠的資源。

非空性

全部對象都必須不等於 null。

高質量的 Equals 方法

1.使用 == 操做符檢查「參數是否爲這個對象的引用」,若是比較操做有可能很昂貴,就值得先這麼作;

2.使用instanceof檢查「參數類型是否正確」;

3.把參數轉換成正確的類型,在instanceof後,因此會保證成功;

4.對每一個關鍵域,檢查參數中的域是否與該對象中對應的域相匹配,float 和 double須要用 Float.compare 和 Double.compare 比較,集合用 Arrays.equals 比較;

5.對於引用域能夠爲 null 的狀況,爲了不致使 NPE,能夠寫成 :

(field == o.filed || (field != null && field.equals(o.field)))

6.最早比較最有可能不一致的域,或者是開銷低的域,最好是同時知足這兩個條件;

7.重寫 equals 時總要重寫 hashCode;

8.不要讓 equals 方法過於智能;

9.不要將 equals 聲明中的 Object 對象替換爲其餘類型;

10.最好用 @Override 註解描述 equals;

相關文章
相關標籤/搜索