1. 程式人生 > >如何重寫hashCode()和equals()方法

如何重寫hashCode()和equals()方法

hashCode()和equals()方法可以說是Java完全面向物件的一大特色.它為我們的程式設計提供便利的同時也帶來了很多危險.這篇文章我們就討論一下如何正解理解和使用這2個方法.

如何重寫equals()方法

如果你決定要重寫equals()方法,那麼你一定要明確這麼做所帶來的風險,並確保自己能寫出一個健壯的equals()方法.一定要注意的一點是,在重寫equals()後,一定要重寫hashCode()方法.具體原因稍候再進行說明. 我們先看看 JavaSE 7 Specification中對equals()方法的說明:
  • It is reflexive: for any non-null reference value x
    x.equals(x) should return true.
  • It is symmetric: for any non-null reference values x and yx.equals(y) should return true if and only if y.equals(x) returns true.
  • It is transitive: for any non-null reference values xy, and z, if x.equals(y) returns true and y.equals(z) returns true, then x.equals(z)
     should return true.
  • It is consistent: for any non-null reference values x and y, multiple invocations of x.equals(y) consistently return true or consistently return false, provided no information used in equals comparisons on the objects is modified.
  • For any non-null reference value xx.equals(null)
     should return false.
這段話用了很多離散數學中的術數.簡單說明一下: 1. 自反性:A.equals(A)要返回true. 2. 對稱性:如果A.equals(B)返回true, 則B.equals(A)也要返回true. 3. 傳遞性:如果A.equals(B)為true, B.equals(C)為true, 則A.equals(C)也要為true. 說白了就是 A = B , B = C , 那麼A = C. 4. 一致性:只要A,B物件的狀態沒有改變,A.equals(B)必須始終返回true. 5. A.equals(null) 要返回false. 相信只要不是專業研究數學的人,都對上面的東西不來電.在實際應用中我們只需要按照一定的步驟重寫equals()方法就可以了.為了說明方便,我們先定義一個程式設計師類(Coder):
class Coder {
	private String name;
	private int age;
	
	// getters and setters
}

我們想要的是,如果2個程式設計師物件的name和age都是相同的,那麼我們就認為這兩個程式設計師是一個人.這時候我們就要重寫其equals()方法.因為預設的equals()實際是判斷兩個引用是否指向內在中的同一個物件,相當於 == . 重寫時要遵循以下三步: 1. 判斷是否等於自身.
if(other == this)
			return true;
2. 使用instanceof運算子判斷 other 是否為Coder型別的物件.
if(!(other instanceof Coder))
			return false;
3. 比較Coder類中你自定義的資料域,name和age,一個都不能少.
Coder o = (Coder)other;
		return o.name.equals(name) && o.age == age;

看到這有人可能會問,第3步中有一個強制轉換,如果有人將一個Integer類的物件傳到了這個equals中,那麼會不會扔ClassCastException呢?這個擔心其實是多餘的.因為我們在第二步中已經進行了instanceof 的判斷,如果other是非Coder物件,甚至other是個null, 那麼在這一步中都會直接返回false, 從而後面的程式碼得不到執行的機會. 上面的三步也是<Effective Java>中推薦的步驟,基本可保證萬無一失.

如何重寫hashCode()方法

在JavaSE 7 Specification中指出, "Note that it is generally necessary to override the hashCode method whenever this method(equals) is overridden, so as to maintain the general contract for the hashCode method, which states that equal objects must have equal hash codes." 如果你重寫了equals()方法,那麼一定要記得重寫hashCode()方法.我們在大學計算機資料結構課程中都已經學過雜湊表(hash table)了,hashCode()方法就是為雜湊表服務的. 當我們在使用形如HashMap, HashSet這樣前面以Hash開頭的集合類時,hashCode()就會被隱式呼叫以來建立雜湊對映關係.稍後我們再對此進行說明.這裡我們先重點關注一下hashCode()方法的寫法. <Effective Java>中給出了一個能最大程度上避免雜湊衝突的寫法,但我個人認為對於一般的應用來說沒有必要搞的這麼麻煩.如果你的應用中HashSet中需要存放上萬上百萬個物件時,那你應該嚴格遵循書中給定的方法.如果是寫一箇中小型的應用,那麼下面的原則就已經足夠使用了: 要保證Coder物件中所有的成員都能在hashCode中得到體現. 對於本例,我們可以這麼寫:
@Override
	public int hashCode() {
		int result = 17;
		result = result * 31 + name.hashCode();
		result = result * 31 + age;
		
		return result;
	}

其中int result = 17你也可以改成20, 50等等都可以.看到這裡我突然有些好奇,想看一下String類中的hashCode()方法是如何實現的.查文件知: "Returns a hash code for this string. The hash code for a String object is computed as
 s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
 
using int arithmetic, where s[i] is the ith character of the string, n is the length of the string, and ^ indicates exponentiation. (The hash value of the empty string is zero.)"
對每個字元的ASCII碼計算n - 1次方然後再進行加和,可見Sun對hashCode的實現是很嚴謹的. 這樣能最大程度避免2個不同的String會出現相同的hashCode的情況.

重寫equals()而不重寫hashCode()的風險

在Oracle的Hash Table實現中引用了bucket的概念.如下圖所示:
從上圖中可以看出,帶bucket的hash table大致相當於雜湊表與連結串列的結合體.即在每個bucket上會掛一個連結串列,連結串列的每個結點都用來存放物件.Java通過hashCode()方法來確定某個物件應該位於哪個bucket中,然後在相應的連結串列中進行查詢.在理想情況下,如果你的hashCode()方法寫的足夠健壯,那麼每個bucket將會只有一個結點,這樣就實現了查詢操作的常量級的時間複雜度.即無論你的物件放在哪片記憶體中,我都可以通過hashCode()立刻定位到該區域,而不需要從頭到尾進行遍歷查詢.這也是雜湊表的最主要的應用. 如: 當我們呼叫HashSet的put(Object o)方法時,首先會根據o.hashCode()的返回值定位到相應的bucket中,如果該bucket中沒有結點,則將 o 放到這裡,如果已經有結點了, 則把 o 掛到連結串列末端.同理,當呼叫contains(Object o)時,Java會通過hashCode()的返回值定位到相應的bucket中,然後再在對應的連結串列中的結點依次呼叫equals()方法來判斷結點中的物件是否是你想要的物件. 下面我們通過一個例子來體會一下這個過程: 我們先建立2個新的Coder物件:
Coder c1 = new Coder("bruce", 10);
		Coder c2 = new Coder("bruce", 10);

假定我們已經重寫了Coder的equals()方法而沒有重寫hashCode()方法:
@Override
	public boolean equals(Object other) {
		System.out.println("equals method invoked!");
		
		if(other == this)
			return true;
		if(!(other instanceof Coder))
			return false;
		
		Coder o = (Coder)other;
		return o.name.equals(name) && o.age == age;
	}

然後我們構造一個HashSet,將c1物件放入到set中:
Set<Coder> set = new HashSet<Coder>();
		set.add(c1);

再執行:
System.out.println(set.contains(c2));

我們期望contains(c2)方法返回true, 但實際上它返回了false. c1和c2的name和age都是相同的,為什麼我把c1放到HashSet中後,再呼叫contains(c2)卻返回false呢?這就是hashCode()在作怪了.因為你沒有重寫hashCode()方法,所以HashSet在查詢c2時,會在不同的bucket中查詢.比如c1放到05這個bucket中了,在查詢c2時卻在06這個bucket中找,這樣當然找不到了.因此,我們重寫hashCode()的目的在於,在A.equals(B)返回true的情況下,A, B 的hashCode()要返回相同的值

我讓hashCode()每次都返回一個固定的數行嗎

有人可能會這樣重寫:
@Override
	public int hashCode() {
		return 10;

	}

如果這樣的話,HashMap, HashSet等集合類就失去了其 "雜湊的意義".用<Effective Java>中的話來說就是,雜湊表退化成了連結串列.如果hashCode()每次都返回相同的數,那麼所有的物件都會被放到同一個bucket中,每次執行查詢操作都會遍歷連結串列,這樣就完全失去了雜湊的作用.所以我們最好還是提供一個健壯的hashCode()為妙.