2020/07/06

[筆記] Effective Java #10 覆寫 equals 時請遵守通用約定(上)

Effective Java 3rd 簡體中文版筆記 #10 覆寫 equals 時請遵守通用約定 
Object class 中所有 non-final 的方法 (equalshashCodetoStringclonefinalizer) 都有明確的通用規則,是設計給開發者覆寫的。如果在覆寫時沒有遵守通則,那麼在調用像是 HashMap/HashSet 中的方法時,可能會無法正常運作。

最簡單避免犯錯的方式,就是不要覆寫 equals,以下幾種情境不需要覆寫 equals
1. class 的每個實體皆視為不同的,像是 Thread 或是 Event
2. class 不需要提供邏輯相等 (logical equality) 的測試功能而覆寫 equals
3. 父類別 (super class) 已經覆寫 equals,而且其行為也符合當前的 class。
4. private class,確定它的 equals 永遠不會被調用。

那麼何時需要覆寫 equals 呢? 當該 class 有特有的邏輯相等時,不是單純比較實體是否相同,而且其父類別的 equals 沒有符合期望,這樣的類通常稱為 value class,像是 IntegerString。有一種 value class 不需要覆寫 equals ,就是每個 value 至多只會有一個實體存在,像是 enum。

覆寫 equals 時,需要注意下面五點,前三項為常見的特性
1. reflexive (反身性):x.equals(x) 必為 true。
2. symmetric (對稱性):若且唯若 (if and only if) x.equals(y) 為 true,則 y.equals(x) 為 true。
3. transitive (遞移性):當 x.equals(y) 為 true 且 y.equals(z) 為 true 時,則 x.equals(z) 必為 true。

Point 代表一個點,ColorPoint 代表有顏色的點,Point 的 equals 比較座標,ColorPoint 的 equals 比較座標及顏色。
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 x == p.x && y == p.y;
    }
}

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 有什麼問題呢? 它違反 symmetric (對稱性),範例如下。
Point p = new Point(1, 2);
ColorPoint cp = new ColorPoint(1, 2, Color.RED);
System.out.println(p.equals(cp));  // true
System.out.println(cp.equals(p));  // false
接著來修正 ColorPointequals
@Override 
public boolean equals(Object o) {
    if (!(o instanceof Point))
        return false;
    // 如果 o 是 Point,就不比較顏色
    if (!(o instanceof ColorPoint))
        return o.equals(this);
    // 如果 o 是 ColorPoint,就比較座標及顏色
    return super.equals(o) && ((ColorPoint)o).color == color;
}
symmetric (對稱性) 修正後,再來試試,結果是這樣的寫法違反 transitive (遞移性)。
Point p = new Point(1, 2);
ColorPoint cp = new ColorPoint(1, 2, Color.RED);
System.out.println(p.equals(cp));  // true
System.out.println(cp.equals(p));  // true

ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
Point p2 = new Point(1, 2);
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);
System.out.println(p1.equals(p2))  // true
System.out.println(p2.equals(p3))  // true
System.out.println(p1.equals(p3))  // false
再修正為另一種寫法,如果不同類就認定為不相等呢? 這樣的寫法破壞里氏替換原則 (Liskov substitution principle),只要繼承 Point 的任何 class 一律不會與 Point 相等,但其實繼承的 subclass 也是 Point 的一種。
里氏替換原則 (Liskov substitution principle)
http://teddy-chen-tw.blogspot.com/2012/01/4.html
@Override 
public boolean equals(Object o) {
    if (o == null || o.getClass() != getClass())
        return false;
    Point p = (Point) o;
    return p.x == x && p.y == y;
}
從上面幾個例子中,結論是無法在繼承非抽象類的 subclass 中,增加新的 value,同時又遵守 equals 的通則。這時就應該放棄使用繼承,考慮複合 (composition)
public class ColorPoint {
    private final Point point;
    private final Color color;

    public ColorPoint(int x, int y, Color color) {
        this.point = new Point(x, y);
        this.color = color;
    }
    ...
}
4. consistent (一致性):在不改變 equals 的前提下,多次調用 x.equals(y) 需為相同結果。
5. 當 x 為 non-null 時,x.equals(null) 必為 false。
在檢查第 5 項時,須注意不可丟出 NullPointerException,將物件強制轉型時,也要先檢查型態,避免丟出 ClassCastException
下一篇 [筆記] Effective Java #10 覆寫 equals 時請遵守通用約定(下), 將討論如果正確的覆寫 equals

轉載請註明原文網址 https://cookieandcoketw.blogspot.com/2020/07/effective-java-10-override-equals-1.html

沒有留言:

張貼留言