第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方法的具體解釋,能夠參考這篇文章:
第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;
}