Effective Java讀書筆記——第三章 對於全部物件都通用的方法

weixin_33686714發表於2017-08-21

第8條:覆蓋equals時請遵守通用的約定

設計Object類的目的就是用來覆蓋的,它全部的非final方法都是用來被覆蓋的(equals、hashcode、clone、finalize)都有通用約定。

首先看看equals方法:

若滿足以下的這些情況中的某一個,您能夠直接使用Object類中的equals方法而不用覆蓋:

  • 類的每個例項本質上是唯一的。對於那些代表例項而不是值的類來說能夠不用覆蓋equals方法。比方Thread類。由於每個Thread類的例項都表示一個執行緒,這與Thread某些域的值沒有關係(我們沒有必要用equals推斷Thread中兩個例項的某個域相等而推斷出Thread相等,這沒有意義。由於每個Thread例項都表示一個執行緒,它們都是唯一的)。

  • 不關心類是否提供了“邏輯相等”的測試功能。

    Random類覆蓋了equals方法,以檢查兩個Random例項是否產生同樣的隨即序列。但這通常沒有意義。

  • 超類已經覆蓋了equals,從超類繼承過來的行為對於子類也是合適的。如,大多數的Set實現都從AbstractSet繼承equals實現,List實現從AbstractList繼承equals實現,Map實現從AbstractMap繼承實現。

  • 類是私有的或是包級私有的。應該確定他的equals方法永不會被呼叫。這樣的情況下,equals方法應該被重寫,以防意外被呼叫。

@Override 
public boolean equals(Object o) {
    throw new AssertionError();
} 

那麼合適應該重寫equals方法呢?假設類具有自己特有的“邏輯相等”的概念(而不是物件的地址相等),並且這個類的超類並沒有覆蓋equals以實現期望的行為,這時應該覆蓋equals方法。這通常屬於“值類(value class)”的情形。 所謂的值類就是指類中僅有一個域的類。如包裝類Integer。或者日期類Date。

當然另一種類不用重寫equals方法,即單例類。

重寫equals方法的規範:

1、自反性:對於隨意非null的引用x , 必有x.equals(x) == true.

2、對稱性:

對於不論什麼非null的引用值x和y。若x.equals(y) == true ,那麼必有y.equals(x) == true。

以下這個類重寫了equals方法,但違反了對稱性:

public final class CaseInsensitiveString {
    private final String s;
    public CaseInsensitiveString(String s) {
        if(s == null) {
            throw new NullPointerExecption();
        }
        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;
    }


}

在呼叫這個類的時候:

CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
String s = "polish";

在呼叫cis.equals(s)時返回true,可是s.equals(cis)將返回false ,由於String類中的equals方法並不知道比較的是不區分大寫和小寫的字串。這明顯違反了自反性。


所以須要這麼改動程式碼:

@Override
public boolean equals(Object o) {
    return o instanceof CaseInsensitiveString && ((CaseInsensitiveString)o).s.equalsIsIgnore(s);
}

3、傳遞性:
假設第一個物件equals第二個物件,第二個物件equals第三個物件,那麼第一個物件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 p.x == x && p.y == y; 
    }

}

以下實現了一個子類:

public class ColorPoint extends Point {
    private final Color color;
    public ColorPoint(int x,int y,Color color) {
        super(x,y);
        this.color = color;
    }

}

假設不重寫equals方法。那麼在比較時就忽略了顏色,這顯然不可接受。
那麼如今重寫equals方法:

@Override
public boolean equals(Object o) {
    if(!(o instanceof ColorPoint)) {
        return false;

    }
    return super.equals(o) && ((ColorPoint)o).color == color;
}

可是這樣重寫有個問題。當我們例項化一個Point和一個ColorPoint時:

Point p = new Point(1,2);
ColorPoint cp = new ColorPoint(1,2,Color.RED);

當呼叫p.equals(cp)時返回true,可是cp.equals(p)是返回false,原因是p並非ColorPoint型別或是其子型別的。那麼可修正這個問題,在ColorPoint.equals進行混合比較時忽略顏色資訊:

@Override
public boolean equals(Object o) {
    if(!(o instanceof Point))
        return false;

    if(!(o instanceof ColorPoint))
        return ((Point)o).equals(this);

    return super.equals(o) && ((ColorPoint)o).color == color;
}

這樣的方式實現了對稱性,卻犧牲了傳遞性:

ColorPoint p1 = new ColorPoint(1,2,Color.RED);
Point p2 = new Point(1,2);
ColorPoint p3 = new ColorPoint(1,2,Color.BLUE);

這樣的情況而來,p1.equals(p2) == true,且p2.equals(p3) == true,可是p1.equals(p3) == false,這違反了傳遞性。

假設這樣寫:

@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;
}

這犧牲了物件導向的優勢。即動態繫結。這要求物件必須有同樣實現。


要編寫一個方法,用來推斷整值點是否在單位圓中:

private static final Set<Point> unitCircle;

static {
    unitCircle = new HashSet<Point>();
    unitCircle.add(new Point(1,0));
    unitCircle.add(new Point(0,1)); 
    unitCircle.add(new Point(-1,0));
    unitCircle.add(new Point(0,-1));
}

public static boolean onUnitCircle(Point p) {
    return unitCircle.contains(p);
}

此時。假設擴充套件了一個新類:

public class CounterPoint extends Point {   
    private static final AtomicInteger counter = new AtomicInteger();
    public CounterPoint(int x,int y) {
        super(x,y);
        counter.incrementAndGet();
    }
    public int numberCreated() {
        return counter.get();

    }

}

假設像上面一樣,重寫的equals方法中使用getClass()推斷,那麼不管怎樣將返回false,這違反了里氏替換原則。


解決的方法是,用組合取代繼承。即在ColorPoint類中新增一個私有的Point域,並新增一個方法用於返回該域:

public class ColorPoint { 
    private final Point point;
    private final Color color;
    public ColorPoint(int x,int y,Color color) {
        if(color == null) {
            throw new NullPointerException();
        }
        point = new Point(x,y);
        this.color = color; 
    }
    public Point asPoint() {
        return point;   
    }
    @Override
    public boolean equals(Object o) {
        if(!(o instanceoc ColorPoint))
            return false;
        ColorPoint cp = (ColorPoint)o;
        return cp.point.equals(point) && cp.color.equals(color);
    }
}

4、一致性

相等的物件永遠相等,不相等的永遠不相等。即。可變物件在不同的時候能夠與不同的物件相等。而不可變物件則不會這樣。

5、非空性:

全部物件都必須不為null。

@Override
public boolean equals(Object o) {
    if(o == null) {
        return false;

    }
} 

事實上這一步是不須要的,直接用instanceof操作符就能夠:

@Override
public boolean equals(Object o) {
    if(!(o instanceof MyType))
        return false;
    MyType mt  = (MyType)o;
}

假設o為null的話。那麼方法直接返回false,假設o不是MyType型別(或其子型別的話)。那麼程式直接丟擲ClassCastException異常。

依據上面的討論。針對equals小結一下幾點:

  • 使用==檢查“引數是否為這個物件的引用”,若是,返回true。這是一種效能優化。若比較非常昂貴,就值得這麼做。

  • 使用instanceof操作符檢查是否為正確的型別。

  • 把引數轉換成正確的型別。由於之前使用了instanceof操作符,所以轉換肯定能夠成功。

  • 對於該類中的每個關鍵的域(significant)。檢查引數中的域是否與該物件中相應的域相匹配。

    ——對於既不是float也不是double的基本型別域,能夠使用==操作符,對於物件引用的域。能夠遞迴呼叫equals方法。對於float域,能夠使用Float.compare方法,對於double域,使用Double.compare方法。

    對於某些物件引用時null的域。能夠用這樣的比較方式(field == null ? o.field == null : field.equals(o.field));**

  • equals比較的順序不同。效率可能不一樣,所以應該先比較開銷較低的域。

  • 覆蓋equals是總要覆蓋hashcode。(後面會講)**

  • 不要將equals宣告中的Object物件替換為其它型別。


第9條:覆蓋equals時總要覆蓋hashcode


對於equals和hashcode之間的關係,能夠先參考這篇文章:
《Java中的equals和hashCode方法具體解釋》


首先看看Object規範:

  • 假設兩個物件依據equals(Object)方法比較是相等的。那麼呼叫這兩個物件中隨意一個物件的hashCode方法都必須產生同樣的整數結果。

  • 假設兩個物件依據equals方法比較是不相等的,那麼呼叫這兩個物件中隨意一個物件的hashCode方法。則有可能產生同樣的結果。但不相等的物件產生截然不同的整數結果,有可能提高雜湊表(hash table)的效能。

考慮以下的類:

public final class PhoneNumber {
    private final short areaCode;
    private final short prefix;
    private final short lineNumber;
    public PhoneNumber(int areaCode,int prefix,int lineNumber) {
        rangeCheck(areaCode,999,"area code");
        rangeCheck(prefix,999,"prefix");
        rangeCheck(lineNumber,9999,"lineNumber");
        this.areaCode = (short)areaCode;
        this.prefix = (short) prefix;
        this.lineNumber = (short)lineNumber;


    }
    private static void rangeCheck(int arg,int max,String name) {
        if(arg < 0 || arg > max) { 
            throw new IllegalArgumentException(name + ": " + arg);


            }

    }
    @Override
    public boolean equals(Object o) {
        if(o == this)
            return true;
        if(!(o instanceof PhoneNumber)) { 
            return false;
        }
        PhoneNumber pn = (PhoneNumber)o;
        return pn.lineNumber == lineNumber && pn.prefix == prefix && pn.areaCode == areaCode;
        //未重寫hashCode方法
        ...


    } 

}

這時,若考慮:

Map<PhoneNumber,String> m = new HashMap<>();
m.put(new PhoneNumber(123,456,789),"Jenny");

假設期望呼叫:

m.get(new PhoneNumber(123,456,789));

返回的是“Jenny”的話。實際上無法做到。由於它返回的是null。由於這裡有兩個PhoneNumber例項,第一個被插入到Map的雜湊桶中,第二個用於獲取該物件,但兩個物件的雜湊碼不同,由於hashCode預設返回的是物件的地址值,get方法會首先推斷Map中是否有與目標物件的hashCode同樣的物件。顯然。這是兩個物件,hashCode明顯不同,於是返回的結果為false。也就找不到了。所以須要重寫hashCode方法。好的重寫方式是為不相等的物件產生不相等的雜湊碼,為相等的物件產生相等的雜湊碼。即假設兩個物件equals為true,那麼兩個物件的hashCode必相等,假設兩個物件equals為false,那麼兩個物件的hashCode不相等。

重寫hashCode:

@Override
public int hashCode() {
    int result = 17;
    result = 31 * result + areaCode;
    result = 31 * result + prefix;
    result = 31 * result + lineNumber;
    return result;
}

假設計算雜湊碼的開銷較大,能夠考慮把hashCode值儲存於物件內部,等須要計算的時候再計算,即懶載入的模式:

private volatile int hashCode;

...

@Override
public int hashCode() {
    int result = hashCode;
    if(result == 0) {
        result = 17;
        result = 31 * result + areaCode;
        result = 31 * result + prefix;
        result = 31 * result + lineNumber;
    }
    return result;
}

第10條:始終要覆蓋toString方法

Object的toString方法預設返回一個“類的名稱@物件雜湊碼的無符號十六進位制數”,這看起來沒什麼意義,所以建議全部的子類都應該覆蓋這種方法。

第11條:慎重地覆蓋clone方法

如須要克隆物件,須要實現Cloneable介面。

有關clone方法的具體解釋,能夠參考這篇文章:

具體解釋Java中的clone方法 – 原型模式


第12條:考慮實現Comparable介面

Comparable介面中唯一方法是compareTo(),該方法同意簡單的比較。並且同意執行順序比較。假設某個類實現了Comparable介面。就表明它的例項具有內在的排序關係,對該物件組成的陣列(或是List)進行排序僅僅需呼叫:

Arrays.sort(a);

Comparable介面的原形:

public interface Comparable<T> {
    int compareTo(T t);

}

將這個物件與指定的物件進行比較。

當該物件小於、等於或大於指定物件的時候,分別返回一個負數、零、正整數。

假設指定的物件的型別與本物件的型別不匹配,則丟擲ClassCastException異常。

建議(x.compareTo(y) == 0) == (x.equals(y))

在使用Comparable介面進行物件之間的比較時,假設該類中有多個域,那麼比較的時候應該依照從最重要的域開始比較。假設不相等則比較結束,返回。假設相等,在比較次要的域,以此類推:

public int compareTo(PhoneNumber pn) {
    int areaCodeDiff = areaCode - pn.areaCode;
    if(areaCode != 0)
        return areaCodeDiff;

    int prefixDiff = prefix - pn.prefix;
    if(prefixDiff != 0)
        return prefixDiff;

    return lineNumber - pn.lineNumber;
}

相關文章