1. 程式人生 > >java-覆蓋equals時規則不容忽視

java-覆蓋equals時規則不容忽視

儘管object是一個具體類,涉及它就是為了擴充套件它,它所有的非final方法(equals,hashCode,toString,clone和finalize)都有一些通用的規定,因為它們被設計就是用來覆蓋(override)的。任何一個類,它在覆蓋這些方法的時候,都有責任遵守這些約定。

本篇文章主要講解覆蓋equals方法需要遵守的規定

覆蓋equals方法看起來很簡單,但是有許多覆蓋方法會導致錯誤,並且後果很嚴重,最容易避免這類問題的方法就是不覆蓋equals方法

那麼什麼時候應該覆蓋Object.equals呢?

如果類具有自己特有的“邏輯相等"概念(不等同物件等同概念),而且超類還沒有覆蓋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。
除非你對數學比較熟悉,不然這些規定確實有點恐懼,但是如果你不遵守它們,你會發現你的程式會變的很不正常,甚至崩潰。 第一個要求必須說明物件等於其自身。 程式碼簡單說明
public final class TestEquals {
	private final String str;
	public TestEquals(String str){
		if(str==null){
			throw new NullPointerException();
		}
		this.str=str;
	}
	@Override
	public boolean equals(Object obj) {
		//這邊只是滿足了規定的第一條
		if(obj instanceof TestEquals){
			return true;
		}
		return false;
	}
}
如果違背了這一條,你用集合類(collection),把這個類的例項新增到集合中,改集合的contains將果斷告訴你,該集合不包含你剛剛新增的例項。 第二個要求是任何兩個物件必須要保持一致 程式碼簡單說明
public final class TestEquals {
	private final String str;

	public TestEquals(String str) {
		if (str == null) {
			throw new NullPointerException();
		}
		this.str = str;
	}

	@Override
	public boolean equals(Object obj) {
		// 這邊只是滿足了規定的第一條和第二條
		return obj instanceof TestEquals
				&& ((TestEquals) obj).str.equalsIgnoreCase(str);
	}
}
如果違背了第二條,當前物件面對你的物件的時候,你完全不知道這些物件的行為時什麼樣的。 第三個是傳遞性,如果一個物件等於第二個物件,第二個物件等於第三個物件,則第一個物件一定等於第三個物件。 可能大家在做面向物件涉及的時候,繼承可能也會用到。 如果父類A實現了equals方法,子類B繼承父類A,如果子類B有自己的屬性,如果子類B沒有實現equals方法,則直接從父類A繼承過來。 然而A.equals(B)返回true。B.equals(A)返回false。 如果子類B重新覆蓋了equals方法。裡面加上了自己屬性相等的條件。然而兩個不同的子類物件B1 B2和一個父類A。B1.equals(A) 和A.equals(B2)都返回true,但是B1.equals(B2)則返回false,違反了傳遞性 因為2個子類不同物件的資料內容肯定不一樣,所以返回false。 怎麼解決呢?事實上,這就是面嚮物件語言中關於等價關係的一個基本問題。我們無法在擴充套件類可例項化的同事,既增加新的值元件,同時保留equals約定。 所以採用:複合優先繼承。讓要繼承的類裡面有父類的引用。就是把原來的繼承關係打破。一個類包含另外一個類的物件。 第四點就是無論類是否可變,都不要是equals方法依靠一些可靠的資源,例如 java.net.URL中的equals方法,依賴對URL中主機的IP地址的比較。將一個主機名轉換IP地址可能需要訪問網路,隨著時間推移,不確保會發生相同的結果。這樣會導致URL的equals方法的一致性(遺憾的是,因為相容性的要求,這一行為無法改變)。 第五點非空性,這點比較好理解,程式碼說明
	@Override
	public boolean equals(Object obj) {
		//滿足非空性
		if(obj==null){
			return false;
	}

所以在覆蓋equals方法的時候不要太智慧,老老實實遵守這些規則,編寫完成,應該問自己三個問題,是否對稱,傳遞和一致。而且equals中中引數的Object物件不要替換成其他的型別。 最後一點,也是最最要的一點,覆蓋equals方法總要覆蓋haseCode。在每個覆蓋了equals方法的類中,則必須覆蓋haseCode方法。如果不這樣做,就會違反Object.haseCode方法,從而導致該類無法結合基於雜湊的集合一起正常工作。 java中基於雜湊的結合包括 HashMap,HashSet和Hashtable。
  1. 相等的物件必須返回相等的雜湊碼。
程式碼簡單說明:
public class TestEquals {
	private String str;

	public TestEquals(String str) {
		if (str == null) {
			throw new NullPointerException();
		}
		this.str = str;
	}

	@Override
	public boolean equals(Object obj) {
		if (obj == null) {
			return false;
		}
		if (obj == this) {
			return true;
		}
		if (!(obj instanceof TestEquals)) {
			return false;
		}
		TestEquals te = (TestEquals) obj;
		if (te.str.equals(str)) {
			return true;
		}
		return false;
	}
}

假如你企圖讓這個類與HashMap一起使用:
		Map<TestEquals,String> m=new HashMap<TestEquals,String>();
		m.put(new TestEquals("13888888888"),"xiaohong");
你可能期望呼叫get方法,通過這個new TestEquals("13888888888")返回“xiaohong"這個字串,但是實際是返回null,這裡涉及2個TestEquals物件。第一插入map中,第二個通過物件獲取名字。這兩個物件相等,但是雜湊碼不相等,違反了haseCode規定,因為你把電話號碼放到一個散利桶中,卻從另外一個雜湊桶中查詢電話號碼,就算在一個雜湊桶中,返回也是null,因為HashMap有一項優化,可以把每個項相關聯的雜湊碼快取起來,如果雜湊碼不相同,也不必要檢驗物件的等同性。 修正這個問題很簡單,就是覆蓋hashCode方法返回相同的雜湊碼。
	@Override
	public int hashCode() {
		return 20;
	}
這樣雖然雜湊碼相同,因為確保了相同的物件有相同的雜湊碼,但是效果極其惡劣,因為它使得每個物件都有相同的雜湊碼,因此,每個物件都對映到同一個雜湊桶中,是散列表退化成連結串列,使得本線性時間執行的程式變成以平方級時間在執行,對於很大的資料量,會關係到散列表能否正常執行。 好的雜湊函式應該為不相等的物件產生不同的雜湊碼 所以要覆蓋hashCode方法,必須要實現把集中不相等的例項均勻分佈到所有的雜湊值上。想達到這理想的情況非常困難。所以接近這種理想的狀態,也可以實現,網上大家搜搜應該有相應的解決方法,但是。 編寫雜湊函式式個研究課題,最後還是留個數學家和理論方面的電腦科學家來完成。