Effective Java筆記第二章對所有對象都通用的方法
第一節覆寫equals時請遵守通用約定
如果對于不同類型的常,變量在JVM中的存儲位置或者equals與==的差別不太清楚的話,強烈建議先讀或者遇到不懂的情況下讀一下這篇文章不同類型的常,變量在JVM中的存儲位置和equals與==的差別,相信一定會對你有一些幫助。
1.最容易避免覆寫equals方法錯誤的辦法就是不覆寫equals方法,在這種情況下,類的每個執行個體都隻會與它自身相等,滿足以下任何一個條件都不需要覆寫equals方法:
1)類的每個執行個體本質上都是唯一的:對于代表活動實體而不是值的類來說就是這樣。
2)不關心類是否提供了"邏輯相等"的測試功能。
3)超類(被繼承的類)已經覆寫了equals,從超類繼承過來的行為對于子類也是适合的。
4)類是私有的或者包級私有的(未指定通路級别,沒有被private,protected或public修飾的類,接口,枚舉,字段以及方法等),通路的級别為(public > 包級私有 > protected > private),可以确定他的equals方法永遠不會被調用。
2.如果類具有自己特有的"邏輯相等"概念(不同于對象等同的概念),而且超類還沒有覆寫equals以實作期望的行為,這時候我們就需要覆寫equals方法,這通常屬于"值類"(僅僅是一個表示值的類,如Integer或Date等)的情形。程式員在利用equals方法來比較值對象的引用時,希望知道他們在邏輯上是否相等,而不是想了解他們是否指向同一個對象。
有一種"值類"不需要覆寫equals方法,即執行個體受控確定"每個值至多隻存在一個對象"的類。枚舉類型就屬于這種類,對于這種類而言,邏輯相同和對象等同是一回事。
3.覆寫equals方法須遵守的規範,equals方法實作了等價關系:
1)自反性:對于任何非null的引用值x,x.equals(x)必須傳回true。
2)對稱性:對于任何非null的引用值x和y,當且僅當y.equals(x)傳回true時,x.equals(y)必須傳回true。
3)傳遞性:對于任何非null的引用值x,y和z,如果x.equals(y)傳回true,并且y.equals(z)也傳回true,那麼x.equals(z)也必須傳回true。
4)一緻性:對于任何非null的引用值x和y,隻要equals的比較操作在對象中所用的資訊沒有被修改,多次調用x.equals(y)就會一緻地傳回true,或者一緻地傳回false。
5)對于任何非null的引用值x,x.equals(null)必須傳回false。
4.逐一詳細叙述覆寫equals方法所需遵守的規範。
1)自反性:對象必須等于其自身,這一點很難無意識地違反。假如違背了這一條,可以把該類的執行個體添加到集合中,該集合的contains方法會果斷的告訴你,該集合不包含你剛剛添加的執行個體。
2)對稱性:關于兩個對象的"他們是否相等"的問題必須保持一緻性,無意識的違反這一條不難想象,例如以下這個例子:
//對稱性
public final class DemoSymmetry {
private final String s;
public DemoSymmetry(String s) {
if (null == s) {
throw new NullPointerException();
}
this.s = s;
}
/**
* 違反對稱(當且僅當x.equals(y)傳回true時,y.equals(x)必須也傳回true)
* 如果這樣寫的話調用DemoSymmetry對象的equals方法時,傳入的是字元串的話會傳回true,但是反過來
* 字元串.equalsDemoSymmetry對象的時候,調用的是String類的方法,傳回false,違反對稱性。
* @param o
* @return
*/
// @Override
// public boolean equals(Object o) {
// if (o instanceof DemoSymmetry) {
// return s.equalsIgnoreCase(((DemoSymmetry) o).s);
// }
// //單向的互操作性
// //instanceof是java中的二進制運算符,左邊是對象,右邊是類。作用為判斷左邊的對象是否是右邊的類的執行個體,但是要注意,左邊的對象執行個體不能是基礎資料類型。
// //類的執行個體包含本身的執行個體以及所有直接或間接子類的執行個體
// if (o instanceof String) {
// //equalsIgnoreCase忽略大小寫,equals不忽略大小寫
// return s.equalsIgnoreCase((String) o);
// }
// return false;
// }
//修改後的寫法
@Override
public boolean equals(Object o) {
//隻能這樣寫,會先判斷左側的o不是DemoSymmetry的執行個體,是以不是直接false,跳過右邊的判斷。
//判斷順序不能颠倒,不然o不是DemoSymmetry的執行個體的話,會爆類型轉換錯誤,
return o instanceof DemoSymmetry && ((DemoSymmetry) o).s.equalsIgnoreCase(s);
}
public static void main(String[] args) {
//demo是一個類的執行個體
DemoSymmetry demo = new DemoSymmetry("Polish");
//s是一個字元串
String s = "polish";
//執行個體和字元串的值不對等
//這裡是調用DemoSymmetry類中的equals方法進行判斷
boolean equals = demo.equals(s);
System.out.println(equals);
//這裡是調用String類的equals方法進行判斷
boolean equals1 = s.equals(equals);
System.out.println(equals1);
}
}
記住一旦違反了equals約定,當其他對象面臨你的對象時,你完全不知道這些對象的行為會怎麼樣。
3)傳遞性:關于第一個對象等于第二個對象,第二個對象又等于第三個對象,則第一個對象一定等于第三個對象。無意識的違反這條也不難想象,例如以下這個例子:
超類(被繼承的類)
//傳遞性
public class DemoTransitivity {
private final int x;
private final int y;
public DemoTransitivity(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof DemoTransitivity)) {
return false;
}
DemoTransitivity demo = (DemoTransitivity) o;
return demo.x == x && demo.y == y;
}
}
子類
//子類
public class DemoTransitivitySon extends DemoTransitivity {
private final Color color;
public DemoTransitivitySon(int x, int y, Color color) {
super(x, y);
this.color = color;
}
/**
* 違反對稱性
* 父類equals子類的時候會忽略顔色,是以是true,
* 但是子類equals父類的時候,因為顔色參數沒有,是以總是false
*
* @param o
* @return
*/
// @Override
// public boolean equals(Object o) {
// if (!(o instanceof DemoTransitivity)) {
// return false;
// }
// return super.equals(o) && ((DemoTransitivitySon) o).color == color;
// }
/**
* 違反傳遞性
* 父類equals子類的時候會忽略顔色,是以是true,
* 子類equals父類的時候,調用return o.equals(this),就等于o(父類)equals(this)(子類),不區分顔色,是以還是true
* 但是子類equals子類的時候,顔色不同就會是是false
*
* @param
*/
@Override
public boolean equals(Object o) {
if (!(o instanceof DemoTransitivity)) {
return false;
}
if (!(o instanceof DemoTransitivitySon)) {
return o.equals(this);
}
return super.equals(o) && ((DemoTransitivitySon) o).color == color;
}
public static void main(String[] args) {
//測試對稱性
// DemoTransitivitySon ds = new DemoTransitivitySon(1, 2, Color.red);
// DemoTransitivity df = new DemoTransitivity(1, 2);
// System.out.println(df.equals(ds));//true
// System.out.println(ds.equals(df));//false
//測試傳遞性
DemoTransitivitySon demo1 = new DemoTransitivitySon(1, 2, Color.red);
DemoTransitivity demo2 = new DemoTransitivity(1, 2);
DemoTransitivitySon demo3 = new DemoTransitivitySon(1, 2, Color.blue);
System.out.println(demo1.equals(demo2));//true
System.out.println(demo2.equals(demo3));//true
System.out.println(demo1.equals(demo3));//false
}
這是面向對象語言中關于等價關系的一個基本問題:我們無法在擴充可執行個體化的類的同時,既增加新的值元件(屬性),同時又保留equals約定,除非願意放棄面向對象的抽象所帶來的優勢。
你可能聽說,在equals方法中用getClass測試代替instanceof測試,可以擴充可執行個體化的類和增加新的值元件,同時保留equals約定:
@Override
public boolean equals(Object o) {
//getClass傳回調用該方法的對象的類
if (null == o || o.getClass() != getClass()) {
return false;
}
DemoTransitivity demo = (DemoTransitivity) o;
return demo.x == x && demo.y == y;
}
注意,這段程式隻有當對象具有相同的實作時,才能使對象等同。這樣雖然不算太糟糕,但是結果卻是無法接受的。
假設我們要編寫一個方法,用來檢測某個整值點是否在機關圓中,采用以下的方法:
public class DemoTransitivityTest {
private static final Set<DemoTransitivity> unitCircle;
static {
unitCircle = new HashSet<>();
unitCircle.add(new DemoTransitivity(1, 0));
unitCircle.add(new DemoTransitivity(0, 1));
unitCircle.add(new DemoTransitivity(-1, 0));
unitCircle.add(new DemoTransitivity(0, -1));
}
public static boolean onUnitCircle(DemoTransitivity dt) {
return unitCircle.contains(dt);
}
}
假設你通過某種不添加值元件的方式擴充了DemoTransitivity方法,比如說讓他的構造器記錄建立了多少個執行個體:
public class DemoTransitivityCounter extends DemoTransitivity {
private static final AtomicInteger counter = new AtomicInteger();
public DemoTransitivityCounter(int x, int y) {
super(x, y);
counter.incrementAndGet();
}
public int numberCreated() {
return counter.get();
}
}
首先根據裡氏替換原則,一個類型的任何重要屬性也将适用于他的子類型,是以為該類型編寫的任何方法,在他的子類型上也應該同樣運作的很好。但是假設我們把DemoTransitivityCounter類的執行個體傳給了DemoTransitivityTest類的onUnitCircle方法,如果DemoTransitivity類使用了基于getClass的equals方法時,該方法隻有當對象具有相同的實作時,才能使對象等同。無論DemoTransitivityCounter執行個體的x和y傳入什麼值,onUnitCircle方法都會傳回false,因為onUnitCircle方法所用的是HashSet集合,利用equals方法檢驗包含條件,沒有任何DemoTransitivityCounter執行個體與任何DemoTransitivity對應。但是如果DemoTransitivity中使用适當的基于instanceof的equals方法時,當遇到
DemoTransitivityCounter時,相同的onUnitCircle方法就會工作的很好。
雖然沒有一種令人滿意的方法可以既擴充不可執行個體化類,有增加值元件,但是有一種不錯的權宜之計:複合優先于繼承,我們不再讓DemoTransitivitySon擴充DemoTransitivity,而是在DemoTransitivitySon中加入一個私有的DemoTransitivity域,以及一個共有的視圖方法,此方法傳回一個與該有色點處在相同位置的普通DemoTransitivity對象。如下:
public class DemoTransitivityModified {
//加入私有的DemoTransitivity域
private final DemoTransitivity dt;
private final Color color;
public DemoTransitivityModified(int x, int y, Color color) {
if (null == color) {
throw new NullPointerException();
}
dt = new DemoTransitivity(x, y);
this.color = color;
}
/**
* 建立公有視圖
* @return
*/
public DemoTransitivity returnDemoTransitivity() {
return dt;
}
/**
* 這樣寫相當于隻有對象是DemoTransitivityModified類的執行個體化時才有可能使用equals判斷相同
* dtm.dt.equals(dt)相當于調用DemoTransitivity的equals方法進行判斷,
* dtm.color.equals(color)相當于調用Color的equals方法進行判斷。
* @param o
* @return
*/
@Override
public boolean equals(Object o) {
if (!(o instanceof DemoTransitivityModified)) {
return false;
}
DemoTransitivityModified dtm = (DemoTransitivityModified) o;
return dtm.dt.equals(dt) && dtm.color.equals(color);
}
}
注意,你可以在一個抽象類的子類中增加新的值元件,而不會違反equals約定。對于用類層次代替标簽類而得到的那種類層次結構來說,這一點很重要。
4)一緻性:如果兩個對象相等,他們必須始終保持相等,除非他們中有一個(或者兩個都)被修改了。可變對象在不同的時候可以與不同的對象相等,而對于不可變對象來說相等的對象永遠相等,不相等的對象永遠不相等。
無論類是否是不可變的,都不要使equals方法依賴于不可靠的資源。
5)非空性:所有的對象都必須不等于null。這個一般沒有必要,因為為了測試參數的等同性,equals方法必須先把參數轉換成合适的類型,以便可以調用他的通路方法,或者通路他的域,在進行轉換之前,equals方法必須使用instanceof操作符,對參數進行判斷,比如:
@Override
public boolean equals(Object o) {
if (!(o instanceof DemoTransitivity)) {
return false;
}
DemoTransitivity demo = (DemoTransitivity) o;
return demo.x == x && demo.y == y;
}
5.實作高品質equals方法的訣竅。
1)使用"==“操作符檢查"參數是否為這個對象的引用”。如果是,則傳回true。這是一種性能優化,如果操作比較昂貴,就值得這麼做。
2)使用instanceof操作符檢查"參數是否為正确的類型",如果不是則傳回false。
3)把參數轉換成正确的類型。因為轉換之前進行過instanceof測試,是以確定會成功。
4)對于該類中的每個"關鍵域",檢查參數中的域(域是一種屬性,可以是一個類變量,一個對象變量,一個對象方法變量或者是一個函數的參數)是否與該對象中對應的域比對。
對于既不是float也不是double類型的基本類型域,可以使用" = ="操作符進行比較,對于對象引用域,可以遞歸地調用equals方法,對于float域,可以使用Float.compare方法,對于double域,則使用Double.compare。
域的比較順序可能會影響到equals方法的性能。為了獲得最佳的性能,應該最先比較最有可能不一緻的域,或者開銷最低的域,最理想的情況是兩個條件同時滿足的域。
5)當你編寫完成了equals方法之後,應該問自己三個問題:他們是否是對稱的,傳遞的,一緻的?
6.告誡:
1)覆寫equals時總要覆寫hashCode。
2)不要企圖讓equals方法過于智能。想要多度的尋求各種等價關系,很容易就陷入麻煩之中。
3)不要将equals聲明中的Object對象替換為其他類型。