重寫equal()時爲什麼也得重寫hashCode()之深度解讀equal方法與hashCode方法淵源

http://blog.csdn.net/javazejian/article/details/51348320

今天這篇文章我們打算來深度解讀一下equal方法以及其關聯方法hashCode(),我們準備從以下幾點入手分析:


1.equals()的所屬以及內部原理(即Object中equals方法的實現原理)

說起equals方法,我們都知道是超類Object中的一個基本方法,用於檢測一個對象是否與另外一個對象相等。而在Object類中這個方法實際上是判斷兩個對象是否具有相同的引用,如果有,它們就一定相等。其源碼如下:

  1. public boolean equals(Object obj) {   return (this == obj);     }  
public boolean equals(Object obj) {   return (this == obj);     }

實際上我們知道所有的對象都擁有標識(內存地址)和狀態(數據),同時“==”比較兩個對象的的內存地址,所以說 Object 的 equals() 方法是比較兩個對象的內存地址是否相等,即若 object1.equals(object2) 爲 true,則表示 equals1 和 equals2 實際上是引用同一個對象。

2.equals()與‘==’的區別

或許這是我們面試時更容易碰到的問題”equals方法與‘==’運算符有什麼區別?“,並且常常我們都會胸有成竹地回答:“equals比較的是對象的內容,而‘==’比較的是對象的地址。”。但是從前面我們可以知道equals方法在Object中的實現也是間接使用了‘==’運算符進行比較的,所以從嚴格意義上來說,我們前面的回答並不完全正確。我們先來看一段代碼並運行再來討論這個問題。

  1. package com.zejian.test;  
  2. public class Car {  
  3.     private int batch;  
  4.     public Car(int batch) {  
  5.         this.batch = batch;  
  6.     }  
  7.     public static void main(String[] args) {  
  8.         Car c1 = new Car(1);  
  9.         Car c2 = new Car(1);  
  10.         System.out.println(c1.equals(c2));  
  11.         System.out.println(c1 == c2);  
  12.     }  
  13. }  
package com.zejian.test;
public class Car {
	private int batch;
	public Car(int batch) {
		this.batch = batch;
	}
	public static void main(String[] args) {
		Car c1 = new Car(1);
		Car c2 = new Car(1);
		System.out.println(c1.equals(c2));
		System.out.println(c1 == c2);
	}
}

運行結果:

false

false

分析:對於‘==’運算符比較兩個Car對象,返回了false,這點我們很容易明白,畢竟它們比較的是內存地址,而c1與c2是兩個不同的對象,所以c1與c2的內存地址自然也不一樣。現在的問題是,我們希望生產的兩輛的批次(batch)相同的情況下就認爲這兩輛車相等,但是運行的結果是儘管c1與c2的批次相同,但equals的結果卻反回了false。當然對於equals返回了false,我們也是心知肚明的,因爲equal來自Object超類,訪問修飾符爲public,而我們並沒有重寫equal方法,故調用的必然是Object超類的原始方equals方法,根據前面分析我們也知道該原始equal方法內部實現使用的是'=='運算符,所以返回了false。因此爲了達到我們的期望值,我們必須重寫Car的equal方法,讓其比較的是對象的批次(即對象的內容),而不是比較內存地址,於是修改如下:

  1. @Override  
  2.     public boolean equals(Object obj) {  
  3.         if (obj instanceof Car) {  
  4.             Car c = (Car) obj;  
  5.             return batch == c.batch;  
  6.         }  
  7.         return false;  
  8.     }  
@Override
	public boolean equals(Object obj) {
		if (obj instanceof Car) {
			Car c = (Car) obj;
			return batch == c.batch;
		}
		return false;
	}

使用instanceof來判斷引用obj所指向的對象的類型,如果obj是Car類對象,就可以將其強制轉爲Car對象,然後比較兩輛Car的批次,相等返回true,否則返回false。當然如果obj不是 Car對象,自然也得返回false。我們再次運行:

true

false

嗯,達到我們預期的結果了。因爲前面的面試題我們應該這樣回答更佳
總結:默認情況下也就是從超類Object繼承而來的equals方法與‘==’是完全等價的,比較的都是對象的內存地址,但我們可以重寫equals方法,使其按照我們的需求的方式進行比較,如String類重寫了equals方法,使其比較的是字符的序列,而不再是內存地址。

3.equals()的重寫規則

前面我們已經知道如何去重寫equals方法來實現我們自己的需求了,但是我們在重寫equals方法時,還是需要注意如下幾點規則的。

  • 自反性。對於任何非null的引用值x,x.equals(x)應返回true。

  • 對稱性。對於任何非null的引用值x與y,當且僅當:y.equals(x)返回true時,x.equals(y)才返回true。

  • 傳遞性。對於任何非null的引用值x、y與z,如果y.equals(x)返回true,y.equals(z)返回true,那麼x.equals(z)也應返回true。

  • 一致性。對於任何非null的引用值x與y,假設對象上equals比較中的信息沒有被修改,則多次調用x.equals(y)始終返回true或者始終返回false。

  • 對於任何非空引用值x,x.equal(null)應返回false。

當然在通常情況下,如果只是進行同一個類兩個對象的相等比較,一般都可以滿足以上5點要求,下面我們來看前面寫的一個例子。

  1. package com.zejian.test;  
  2. public class Car {  
  3.     private int batch;  
  4.     public Car(int batch) {  
  5.         this.batch = batch;  
  6.     }  
  7.     public static void main(String[] args) {  
  8.         Car c1 = new Car(1);  
  9.         Car c2 = new Car(1);  
  10.         Car c3 = new Car(1);  
  11.         System.out.println("自反性->c1.equals(c1):" + c1.equals(c1));  
  12.         System.out.println("對稱性:");  
  13.         System.out.println(c1.equals(c2));  
  14.         System.out.println(c2.equals(c1));  
  15.         System.out.println("傳遞性:");  
  16.         System.out.println(c1.equals(c2));  
  17.         System.out.println(c2.equals(c3));  
  18.         System.out.println(c1.equals(c3));  
  19.         System.out.println("一致性:");  
  20.         for (int i = 0; i < 50; i++) {  
  21.             if (c1.equals(c2) != c1.equals(c2)) {  
  22.                 System.out.println("equals方法沒有遵守一致性!");  
  23.                 break;  
  24.             }  
  25.         }  
  26.         System.out.println("equals方法遵守一致性!");  
  27.         System.out.println("與null比較:");  
  28.         System.out.println(c1.equals(null));  
  29.     }  
  30.     @Override  
  31.     public boolean equals(Object obj) {  
  32.         if (obj instanceof Car) {  
  33.             Car c = (Car) obj;  
  34.             return batch == c.batch;  
  35.         }  
  36.         return false;  
  37.     }  
  38. }  
package com.zejian.test;
public class Car {
	private int batch;
	public Car(int batch) {
		this.batch = batch;
	}
	public static void main(String[] args) {
		Car c1 = new Car(1);
		Car c2 = new Car(1);
		Car c3 = new Car(1);
		System.out.println("自反性->c1.equals(c1):" + c1.equals(c1));
		System.out.println("對稱性:");
		System.out.println(c1.equals(c2));
		System.out.println(c2.equals(c1));
		System.out.println("傳遞性:");
		System.out.println(c1.equals(c2));
		System.out.println(c2.equals(c3));
		System.out.println(c1.equals(c3));
		System.out.println("一致性:");
		for (int i = 0; i < 50; i++) {
			if (c1.equals(c2) != c1.equals(c2)) {
				System.out.println("equals方法沒有遵守一致性!");
				break;
			}
		}
		System.out.println("equals方法遵守一致性!");
		System.out.println("與null比較:");
		System.out.println(c1.equals(null));
	}
	@Override
	public boolean equals(Object obj) {
		if (obj instanceof Car) {
			Car c = (Car) obj;
			return batch == c.batch;
		}
		return false;
	}
}

運行結果:

自反性->c1.equals(c1):true

對稱性:

true

true

傳遞性:

true

true

true

一致性:

equals方法遵守一致性!

與null比較:

false

由運行結果我們可以看出equals方法在同一個類的兩個對象間的比較還是相當容易理解的。但是如果是子類與父類混合比較,那麼情況就不太簡單了。下面我們來看看另一個例子,首先,我們先創建一個新類BigCar,繼承於Car,然後進行子類與父類間的比較。

  1. package com.zejian.test;  
  2. public class BigCar extends Car {  
  3.     int count;  
  4.     public BigCar(int batch, int count) {  
  5.         super(batch);  
  6.         this.count = count;  
  7.     }  
  8.     @Override  
  9.     public boolean equals(Object obj) {  
  10.         if (obj instanceof BigCar) {  
  11.             BigCar bc = (BigCar) obj;  
  12.             return super.equals(bc) && count == bc.count;  
  13.         }  
  14.         return false;  
  15.     }  
  16.     public static void main(String[] args) {  
  17.         Car c = new Car(1);  
  18.         BigCar bc = new BigCar(120);  
  19.         System.out.println(c.equals(bc));  
  20.         System.out.println(bc.equals(c));  
  21.     }  
  22. }  
package com.zejian.test;
public class BigCar extends Car {
	int count;
	public BigCar(int batch, int count) {
		super(batch);
		this.count = count;
	}
	@Override
	public boolean equals(Object obj) {
		if (obj instanceof BigCar) {
			BigCar bc = (BigCar) obj;
			return super.equals(bc) && count == bc.count;
		}
		return false;
	}
	public static void main(String[] args) {
		Car c = new Car(1);
		BigCar bc = new BigCar(1, 20);
		System.out.println(c.equals(bc));
		System.out.println(bc.equals(c));
	}
}

運行結果:

true

false

對於這樣的結果,自然是我們意料之中的啦。因爲BigCar類型肯定是屬於Car類型,所以c.equals(bc)肯定爲true,對於bc.equals(c)返回false,是因爲Car類型並不一定是BigCar類型(Car類還可以有其他子類)。嗯,確實是這樣。但如果有這樣一個需求,只要BigCar和Car的生產批次一樣,我們就認爲它們兩個是相當的,在這樣一種需求的情況下,父類(Car)與子類(BigCar)的混合比較就不符合equals方法對稱性特性了。很明顯一個返回true,一個返回了false,根據對稱性的特性,此時兩次比較都應該返回true纔對。那麼該如何修改才能符合對稱性呢?其實造成不符合對稱性特性的原因很明顯,那就是因爲Car類型並不一定是BigCar類型(Car類還可以有其他子類),在這樣的情況下(Car instanceof BigCar)永遠返回false,因此,我們不應該直接返回false,而應該繼續使用父類的equals方法進行比較才行(因爲我們的需求是批次相同,兩個對象就相等,父類equals方法比較的就是batch是否相同)。因此BigCar的equals方法應該做如下修改:

  1. @Override  
  2. public boolean equals(Object obj) {  
  3.     if (obj instanceof BigCar) {  
  4.         BigCar bc = (BigCar) obj;  
  5.         return super.equals(bc) && count == bc.count;  
  6.     }  
  7.     return super.equals(obj);  
  8. }  
 @Override
	public boolean equals(Object obj) {
		if (obj instanceof BigCar) {
			BigCar bc = (BigCar) obj;
			return super.equals(bc) && count == bc.count;
		}
		return super.equals(obj);
	}
這樣運行的結果就都爲true了。但是到這裏問題並沒有結束,雖然符合了對稱性,卻還沒符合傳遞性,實例如下:
  1. package com.zejian.test;  
  2. public class BigCar extends Car {  
  3.     int count;  
  4.     public BigCar(int batch, int count) {  
  5.         super(batch);  
  6.         this.count = count;  
  7.     }  
  8.     @Override  
  9.     public boolean equals(Object obj) {  
  10.         if (obj instanceof BigCar) {  
  11.             BigCar bc = (BigCar) obj;  
  12.             return super.equals(bc) && count == bc.count;  
  13.         }  
  14.         return super.equals(obj);  
  15.     }  
  16.     public static void main(String[] args) {  
  17.         Car c = new Car(1);  
  18.         BigCar bc = new BigCar(120);  
  19.         BigCar bc2 = new BigCar(122);  
  20.         System.out.println(bc.equals(c));  
  21.         System.out.println(c.equals(bc2));  
  22.         System.out.println(bc.equals(bc2));  
  23.     }  
  24. }  
package com.zejian.test;
public class BigCar extends Car {
	int count;
	public BigCar(int batch, int count) {
		super(batch);
		this.count = count;
	}
	@Override
	public boolean equals(Object obj) {
		if (obj instanceof BigCar) {
			BigCar bc = (BigCar) obj;
			return super.equals(bc) && count == bc.count;
		}
		return super.equals(obj);
	}
	public static void main(String[] args) {
		Car c = new Car(1);
		BigCar bc = new BigCar(1, 20);
		BigCar bc2 = new BigCar(1, 22);
		System.out.println(bc.equals(c));
		System.out.println(c.equals(bc2));
		System.out.println(bc.equals(bc2));
	}
}

運行結果:

true

true

false

bc,bc2,c的批次都是相同的,按我們之前的需求應該是相等,而且也應該符合equals的傳遞性纔對。但是事實上運行結果卻不是這樣,違背了傳遞性。出現這種情況根本原因在於:

  • 父類與子類進行混合比較。

  • 子類中聲明瞭新變量,並且在子類equals方法使用了新增的成員變量作爲判斷對象是否相等的條件。

只要滿足上面兩個條件,equals方法的傳遞性便失效了。而且目前並沒有直接的方法可以解決這個問題。因此我們在重寫equals方法時這一點需要特別注意。雖然沒有直接的解決方法,但是間接的解決方案還說有滴,那就是通過組合的方式來代替繼承,還有一點要注意的是組合的方式並非真正意義上的解決問題(只是讓它們間的比較都返回了false,從而不違背傳遞性,然而並沒有實現我們上面batch相同對象就相等的需求),而是讓equals方法滿足各種特性的前提下,讓代碼看起來更加合情合理,代碼如下:

  1. package com.zejian.test;  
  2. public class Combination4BigCar {  
  3.     private Car c;  
  4.     private int count;  
  5.     public Combination4BigCar(int batch, int count) {  
  6.         c = new Car(batch);  
  7.         this.count = count;  
  8.     }  
  9.     @Override  
  10.     public boolean equals(Object obj) {  
  11.         if (obj instanceof Combination4BigCar) {  
  12.             Combination4BigCar bc = (Combination4BigCar) obj;  
  13.             return c.equals(bc.c) && count == bc.count;  
  14.         }  
  15.         return false;  
  16.     }  
  17. }  
package com.zejian.test;
public class Combination4BigCar {
	private Car c;
	private int count;
	public Combination4BigCar(int batch, int count) {
		c = new Car(batch);
		this.count = count;
	}
	@Override
	public boolean equals(Object obj) {
		if (obj instanceof Combination4BigCar) {
			Combination4BigCar bc = (Combination4BigCar) obj;
			return c.equals(bc.c) && count == bc.count;
		}
		return false;
	}
}

從代碼來看即使batch相同,Combination4BigCar類的對象與Car類的對象間的比較也永遠都是false,但是這樣看起來也就合情合理了,畢竟Combination4BigCar也不是Car的子類,因此equals方法也就沒必要提供任何對Car的比較支持,同時也不會違背了equals方法的傳遞性。

4.equals()的重寫規則之必要性深入解讀

前面我們一再強調了equals方法重寫必須遵守的規則,接下來我們就是分析一個反面的例子,看看不遵守這些規則到底會造成什麼樣的後果。

  1. package com.zejian.test;  
  2. import java.util.ArrayList;  
  3. import java.util.List;  
  4. /** * 反面例子 * @author zejian */  
  5. public class AbnormalResult {  
  6.     public static void main(String[] args) {  
  7.         List<A> list = new ArrayList<A>();  
  8.         A a = new A();  
  9.         B b = new B();  
  10.         list.add(a);  
  11.         System.out.println("list.contains(a)->" + list.contains(a));  
  12.         System.out.println("list.contains(b)->" + list.contains(b));  
  13.         list.clear();  
  14.         list.add(b);  
  15.         System.out.println("list.contains(a)->" + list.contains(a));  
  16.         System.out.println("list.contains(b)->" + list.contains(b));  
  17.     }  
  18.     static class A {  
  19.         @Override  
  20.         public boolean equals(Object obj) {  
  21.             return obj instanceof A;  
  22.         }  
  23.     }  
  24.     static class B extends A {  
  25.         @Override  
  26.         public boolean equals(Object obj) {  
  27.             return obj instanceof B;  
  28.         }  
  29.     }  
  30. }  
package com.zejian.test;
import java.util.ArrayList;
import java.util.List;
/** * 反面例子 * @author zejian */
public class AbnormalResult {
	public static void main(String[] args) {
		List<A> list = new ArrayList<A>();
		A a = new A();
		B b = new B();
		list.add(a);
		System.out.println("list.contains(a)->" + list.contains(a));
		System.out.println("list.contains(b)->" + list.contains(b));
		list.clear();
		list.add(b);
		System.out.println("list.contains(a)->" + list.contains(a));
		System.out.println("list.contains(b)->" + list.contains(b));
	}
	static class A {
		@Override
		public boolean equals(Object obj) {
			return obj instanceof A;
		}
	}
	static class B extends A {
		@Override
		public boolean equals(Object obj) {
			return obj instanceof B;
		}
	}
}

上面的代碼,我們聲明瞭 A,B兩個類,注意必須是static,否則無法被main調用。B類繼承A,兩個類都重寫了equals方法,但是根據我們前面的分析,這樣重寫是沒有遵守對稱性原則的,我們先來看看運行結果:

list.contains(a)->true

list.contains(b)->false

list.contains(a)->true

list.contains(b)->true

19行和24行的輸出沒什麼好說的,將a,b分別加入list中,list中自然會含有a,b。但是爲什麼20行和23行結果會不一樣呢?我們先來看看contains方法內部實現

  1. @Override         
  2. public boolean contains(Object o) {   
  3.      return indexOf(o) != -1;   
  4.  }  
@Override       
public boolean contains(Object o) { 
     return indexOf(o) != -1; 
 }
進入indexof方法
  1.       @Override  
  2. ublic int indexOf(Object o) {  
  3. E[] a = this.a;  
  4. if (o == null) {  
  5.     for (int i = 0; i < a.length; i++)  
  6.         if (a[i] == null)  
  7.             return i;  
  8. else {  
  9.     for (int i = 0; i < a.length; i++)  
  10.         if (o.equals(a[i]))  
  11.             return i;  
  12. }  
  13. return -1;  
        @Override
	public int indexOf(Object o) {
		E[] a = this.a;
		if (o == null) {
			for (int i = 0; i < a.length; i++)
				if (a[i] == null)
					return i;
		} else {
			for (int i = 0; i < a.length; i++)
				if (o.equals(a[i]))
					return i;
		}
		return -1;
	}

可以看出最終調用的是對象的equals方法,所以當調用20行代碼list.contains(b)時,實際上調用了

b.equals(a[i]),a[i]是集合中的元素集合中的類型而且爲A類型(只添加了a對象),雖然B繼承了A,但此時

  1. a[i] instanceof B  
a[i] instanceof B
結果爲false,equals方法也就會返回false;而當調用23行代碼list.contains(a)時,實際上調用了a.equal(a[i]),其中a[i]是集合中的元素而且爲B類型(只添加了b對象),由於B類型肯定是A類型(B繼承了A),所以
  1. a[i] instanceof A  
a[i] instanceof A
結果爲true,equals方法也就會返回true,這就是整個過程。但很明顯結果是有問題的,因爲我們的 list的泛型是A,而B又繼承了A,此時無論加入了a還是b,都屬於同種類型,所以無論是contains(a),還是contains(b)都應該返回true纔算正常。而最終卻出現上面的結果,這就是因爲重寫equals方法時沒遵守對稱性原則導致的結果,如果沒遵守傳遞性也同樣會造成上述的結果。當然這裏的解決方法也比較簡單,我們只要將B類的equals方法修改一下就可以了。
  1. static class B extends A{  
  2.         @Override  
  3.         public boolean equals(Object obj) {  
  4.             if(obj instanceof B){  
  5.                 return true;  
  6.             }  
  7.             return super.equals(obj);  
  8.         }  
  9.     }  
static class B extends A{
		@Override
		public boolean equals(Object obj) {
			if(obj instanceof B){
				return true;
			}
			return super.equals(obj);
		}
	}

到此,我們也應該明白了重寫equals必須遵守幾點原則的重要性了。當然這裏不止是list,只要是java集合類或者java類庫中的其他方法,重寫equals不遵守5點原則的話,都可能出現意想不到的結果。

5.爲什麼重寫equals()的同時還得重寫hashCode()

這個問題之前我也很好奇,不過最後還是在書上得到了比較明朗的解釋,當然這個問題主要是針對映射相關的操作(Map接口)。學過數據結構的同學都知道Map接口的類會使用到鍵對象的哈希碼,當我們調用put方法或者get方法對Map容器進行操作時,都是根據鍵對象的哈希碼來計算存儲位置的,因此如果我們對哈希碼的獲取沒有相關保證,就可能會得不到預期的結果。在java中,我們可以使用hashCode()來獲取對象的哈希碼,其值就是對象的存儲地址,這個方法在Object類中聲明,因此所有的子類都含有該方法。那我們先來認識一下hashCode()這個方法吧。hashCode的意思就是散列碼,也就是哈希碼,是由對象導出的一個整型值,散列碼是沒有規律的,如果x與y是兩個不同的對象,那麼x.hashCode()與y.hashCode()基本是不會相同的,下面通過String類的hashCode()計算一組散列碼:

  1. package com.zejian.test;  
  2. public class HashCodeTest {  
  3.     public static void main(String[] args) {  
  4.         int hash=0;  
  5.         String s="ok";  
  6.         StringBuilder sb =new StringBuilder(s);  
  7.           
  8.         System.out.println(s.hashCode()+"  "+sb.hashCode());  
  9.           
  10.         String t = new String("ok");  
  11.         StringBuilder tb =new StringBuilder(s);  
  12.         System.out.println(t.hashCode()+"  "+tb.hashCode());  
  13.     }  
  14.       
  15. }  
package com.zejian.test;
public class HashCodeTest {
	public static void main(String[] args) {
		int hash=0;
		String s="ok";
		StringBuilder sb =new StringBuilder(s);
		
		System.out.println(s.hashCode()+"  "+sb.hashCode());
		
		String t = new String("ok");
		StringBuilder tb =new StringBuilder(s);
		System.out.println(t.hashCode()+"  "+tb.hashCode());
	}
	
}

運行結果:

3548  1829164700

3548  2018699554

我們可以看出,字符串s與t擁有相同的散列碼,這是因爲字符串的散列碼是由內容導出的。而字符串緩衝sb與tb卻有着不同的散列碼,這是因爲StringBuilder沒有重寫hashCode方法,它的散列碼是由Object類默認的hashCode方法計算出來的對象存儲地址,所以散列碼自然也就不同了。那麼我們該如何重寫出一個較好的hashCode方法呢,其實並不難,我們只要合理地組織對象的散列碼,就能夠讓不同的對象產生比較均勻的散列碼。例如下面的例子:

  1. package com.zejian.test;  
  2. public class Model {  
  3.     private String name;  
  4.     private double salary;  
  5.     private int sex;  
  6.       
  7.     @Override  
  8.     public int hashCode() {  
  9.         return name.hashCode()+new Double(salary).hashCode()   
  10.                 + new Integer(sex).hashCode();  
  11.     }  
  12. }  
package com.zejian.test;
public class Model {
	private String name;
	private double salary;
	private int sex;
	
	@Override
	public int hashCode() {
		return name.hashCode()+new Double(salary).hashCode() 
				+ new Integer(sex).hashCode();
	}
}
上面的代碼我們通過合理的利用各個屬性對象的散列碼進行組合,最終便能產生一個相對比較好的或者說更加均勻的散列碼,當然上面僅僅是個參考例子而已,我們也可以通過其他方式去實現,只要能使散列碼更加均勻(所謂的均勻就是每個對象產生的散列碼最好都不衝突)就行了。不過這裏有點要注意的就是java 7中對hashCode方法做了兩個改進,首先java發佈者希望我們使用更加安全的調用方式來返回散列碼,也就是使用null安全的方法Objects.hashCode(注意不是Object而是java.util.Objects)方法,這個方法的優點是如果參數爲null,就只返回0,否則返回對象參數調用的hashCode的結果。Objects.hashCode 源碼如下:
  1. public static int hashCode(Object o) {  
  2.         return o != null ? o.hashCode() : 0;  
  3.     }  
public static int hashCode(Object o) {
        return o != null ? o.hashCode() : 0;
    }
因此我們修改後的代碼如下:
  1. package com.zejian.test;  
  2. import java.util.Objects;  
  3. public  class Model {  
  4.     private   String name;  
  5.     private double salary;  
  6.     private int sex;  
  7.     @Override  
  8.     public int hashCode() {  
  9.         return Objects.hashCode(name)+new Double(salary).hashCode()   
  10.                 + new Integer(sex).hashCode();  
  11.     }  
  12. }  
package com.zejian.test;
import java.util.Objects;
public  class Model {
	private   String name;
	private double salary;
	private int sex;
	@Override
	public int hashCode() {
		return Objects.hashCode(name)+new Double(salary).hashCode() 
				+ new Integer(sex).hashCode();
	}
}
java 7還提供了另外一個方法java.util.Objects.hash(Object... objects),當我們需要組合多個散列值時可以調用該方法。進一步簡化上述的代碼:
  1. package com.zejian.test;  
  2. import java.util.Objects;  
  3. public  class Model {  
  4.     private   String name;  
  5.     private double salary;  
  6.     private int sex;  
  7. //  @Override  
  8. //  public int hashCode() {  
  9. //      return Objects.hashCode(name)+new Double(salary).hashCode()   
  10. //              + new Integer(sex).hashCode();  
  11. //  }  
  12.       
  13.     @Override  
  14.     public int hashCode() {  
  15.         return Objects.hash(name,salary,sex);  
  16.     }  
  17. }  
package com.zejian.test;
import java.util.Objects;
public  class Model {
	private   String name;
	private double salary;
	private int sex;
//	@Override
//	public int hashCode() {
//		return Objects.hashCode(name)+new Double(salary).hashCode() 
//				+ new Integer(sex).hashCode();
//	}
	
	@Override
	public int hashCode() {
		return Objects.hash(name,salary,sex);
	}
}

好了,到此hashCode()該介紹的我們都說了,還有一點要說的如果我們提供的是一個數值類型的變量的話,那麼我們可以調用Arrays.hashCode()來計算它的散列碼,這個散列碼是由數組元素的散列碼組成的。接下來我們迴歸到我們之前的問題,重寫equals方法時也必須重寫hashCode方法。在Java API文檔中關於hashCode方法有以下幾點規定(原文來自java深入解析一書)。

  • 在java應用程序執行期間,如果在equals方法比較中所用的信息沒有被修改,那麼在同一個對象上多次調用hashCode方法時必須一致地返回相同的整數。如果多次執行同一個應用時,不要求該整數必須相同。

  • 如果兩個對象通過調用equals方法是相等的,那麼這兩個對象調用hashCode方法必須返回相同的整數。

  • 如果兩個對象通過調用equals方法是不相等的,不要求這兩個對象調用hashCode方法必須返回不同的整數。但是程序員應該意識到對不同的對象產生不同的hash值可以提供哈希表的性能。

通過前面的分析,我們知道在Object類中,hashCode方法是通過Object對象的地址計算出來的,因爲Object對象只與自身相等,所以同一個對象的地址總是相等的,計算取得的哈希碼也必然相等,對於不同的對象,由於地址不同,所獲取的哈希碼自然也不會相等。因此到這裏我們就明白了,如果一個類重寫了equals方法,但沒有重寫hashCode方法,將會直接違法了第2條規定,這樣的話,如果我們通過映射表(Map接口)操作相關對象時,就無法達到我們預期想要的效果。如果大家不相信, 可以看看下面的例子(來自java深入解析一書)

  1. package com.zejian.test;  
  2. import java.util.HashMap;  
  3. import java.util.Map;  
  4. public class MapTest {  
  5.     public static void main(String[] args) {  
  6.         Map<String,Value> map1 = new HashMap<String,Value>();  
  7.         String s1 = new String("key");  
  8.         String s2 = new String("key");    
  9.         Value value = new Value(2);  
  10.         map1.put(s1, value);  
  11.         System.out.println("s1.equals(s2):"+s1.equals(s2));  
  12.         System.out.println("map1.get(s1):"+map1.get(s1));  
  13.         System.out.println("map1.get(s2):"+map1.get(s2));  
  14.           
  15.           
  16.         Map<Key,Value> map2 = new HashMap<Key,Value>();  
  17.         Key k1 = new Key("A");  
  18.         Key k2 = new Key("A");  
  19.         map2.put(k1, value);  
  20.         System.out.println("k1.equals(k2):"+s1.equals(s2));  
  21.         System.out.println("map2.get(k1):"+map2.get(k1));  
  22.         System.out.println("map2.get(k2):"+map2.get(k2));  
  23.     }  
  24.       
  25.     /** 
  26.      * 鍵 
  27.      * @author zejian 
  28.      * 
  29.      */  
  30.     static class Key{  
  31.         private String k;  
  32.         public Key(String key){  
  33.             this.k=key;  
  34.         }  
  35.           
  36.         @Override  
  37.         public boolean equals(Object obj) {  
  38.             if(obj instanceof Key){  
  39.                 Key key=(Key)obj;  
  40.                 return k.equals(key.k);  
  41.             }  
  42.             return false;  
  43.         }  
  44.     }  
  45.       
  46.     /** 
  47.      * 值 
  48.      * @author zejian 
  49.      * 
  50.      */  
  51.     static class Value{  
  52.         private int v;  
  53.           
  54.         public Value(int v){  
  55.             this.v=v;  
  56.         }  
  57.           
  58.         @Override  
  59.         public String toString() {  
  60.             return "類Value的值-->"+v;  
  61.         }  
  62.     }  
  63. }  
package com.zejian.test;
import java.util.HashMap;
import java.util.Map;
public class MapTest {
	public static void main(String[] args) {
		Map<String,Value> map1 = new HashMap<String,Value>();
		String s1 = new String("key");
		String s2 = new String("key");	
		Value value = new Value(2);
		map1.put(s1, value);
		System.out.println("s1.equals(s2):"+s1.equals(s2));
		System.out.println("map1.get(s1):"+map1.get(s1));
		System.out.println("map1.get(s2):"+map1.get(s2));
		
		
		Map<Key,Value> map2 = new HashMap<Key,Value>();
		Key k1 = new Key("A");
		Key k2 = new Key("A");
		map2.put(k1, value);
		System.out.println("k1.equals(k2):"+s1.equals(s2));
		System.out.println("map2.get(k1):"+map2.get(k1));
		System.out.println("map2.get(k2):"+map2.get(k2));
	}
	
	/**
	 * 鍵
	 * @author zejian
	 *
	 */
	static class Key{
		private String k;
		public Key(String key){
			this.k=key;
		}
		
		@Override
		public boolean equals(Object obj) {
			if(obj instanceof Key){
				Key key=(Key)obj;
				return k.equals(key.k);
			}
			return false;
		}
	}
	
	/**
	 * 值
	 * @author zejian
	 *
	 */
	static class Value{
		private int v;
		
		public Value(int v){
			this.v=v;
		}
		
		@Override
		public String toString() {
			return "類Value的值-->"+v;
		}
	}
}
代碼比較簡單,我們就不過多解釋了(注意Key類並沒有重寫hashCode方法),直接運行看結果
  1. s1.equals(s2):true  
  2. map1.get(s1):類Value的值-->2  
  3. map1.get(s2):類Value的值-->2  
  4. k1.equals(k2):true  
  5. map2.get(k1):類Value的值-->2  
  6. map2.get(k2):null  
s1.equals(s2):true
map1.get(s1):類Value的值-->2
map1.get(s2):類Value的值-->2
k1.equals(k2):true
map2.get(k1):類Value的值-->2
map2.get(k2):null
對於s1和s2的結果,我們並不驚訝,因爲相同的內容的s1和s2獲取相同內的value這個很正常,因爲String類重寫了equals方法和hashCode方法,使其比較的是內容和獲取的是內容的哈希碼。但是對於k1和k2的結果就不太盡人意了,k1獲取到的值是2,k2獲取到的是null,這是爲什麼呢?想必大家已經發現了,Key只重寫了equals方法並沒有重寫hashCode方法,這樣的話,equals比較的確實是內容,而hashCode方法呢?沒重寫,那就肯定調用超類Object的hashCode方法,這樣返回的不就是地址了嗎?k1與k2屬於兩個不同的對象,返回的地址肯定不一樣,所以現在我們知道調用map2.get(k2)爲什麼返回null了吧?那麼該如何修改呢?很簡單,我們要做也重寫一下hashCode方法即可(如果參與equals方法比較的成員變量是引用類型的,則可以遞歸調用hashCode方法來實現):
  1. @Override  
  2. public int hashCode() {  
  3.      return k.hashCode();  
  4. }  
@Override
public int hashCode() {
     return k.hashCode();
}
再次運行:
  1. s1.equals(s2):true  
  2. map1.get(s1):類Value的值-->2  
  3. map1.get(s2):類Value的值-->2  
  4. k1.equals(k2):true  
  5. map2.get(k1):類Value的值-->2  
  6. map2.get(k2):類Value的值-->2  
s1.equals(s2):true
map1.get(s1):類Value的值-->2
map1.get(s2):類Value的值-->2
k1.equals(k2):true
map2.get(k1):類Value的值-->2
map2.get(k2):類Value的值-->2

6.重寫equals()中getClass與instanceof的區別

雖然前面我們都在使用instanceof(當然前面我們是根據需求(批次相同即相等)而使用instanceof的),但是在重寫equals() 方法時,一般都是推薦使用 getClass 來進行類型判斷(除非所有的子類有統一的語義才使用instanceof),不是使用 instanceof。我們都知道 instanceof 的作用是判斷其左邊對象是否爲其右邊類的實例,返回 boolean 類型的數據。可以用來判斷繼承中的子類的實例是否爲父類的實現。下來我們來看一個例子:父類Person

  1. public class Person {  
  2.         protected String name;  
  3.         public String getName() {  
  4.             return name;  
  5.         }  
  6.         public void setName(String name) {  
  7.             this.name = name;  
  8.         }  
  9.         public Person(String name){  
  10.             this.name = name;  
  11.         }  
  12.         public boolean equals(Object object){  
  13.             if(object instanceof Person){  
  14.                 Person p = (Person) object;  
  15.                 if(p.getName() == null || name == null){  
  16.                     return false;  
  17.                 }  
  18.                 else{  
  19.                     return name.equalsIgnoreCase(p.getName ());  
  20.                 }  
  21.             }  
  22.             return false;  
  23.        }  
  24.     }  
public class Person {
        protected String name;
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        public Person(String name){
            this.name = name;
        }
        public boolean equals(Object object){
            if(object instanceof Person){
                Person p = (Person) object;
                if(p.getName() == null || name == null){
                    return false;
                }
                else{
                    return name.equalsIgnoreCase(p.getName ());
                }
            }
            return false;
       }
    }
子類 Employee
  1. public class Employee extends Person{  
  2.         private int id;  
  3.         public int getId() {  
  4.             return id;  
  5.         }  
  6.         public void setId(int id) {  
  7.             this.id = id;  
  8.         }  
  9.         public Employee(String name,int id){  
  10.             super(name);  
  11.             this.id = id;  
  12.         }  
  13.         /** 
  14.          * 重寫equals()方法 
  15.          */  
  16.         public boolean equals(Object object){  
  17.             if(object instanceof Employee){  
  18.                 Employee e = (Employee) object;  
  19.                 return super.equals(object) && e.getId() == id;  
  20.             }  
  21.             return false;  
  22.         }  
  23.     }  
public class Employee extends Person{
        private int id;
        public int getId() {
            return id;
        }
        public void setId(int id) {
            this.id = id;
        }
        public Employee(String name,int id){
            super(name);
            this.id = id;
        }
        /**
         * 重寫equals()方法
         */
        public boolean equals(Object object){
            if(object instanceof Employee){
                Employee e = (Employee) object;
                return super.equals(object) && e.getId() == id;
            }
            return false;
        }
    }
上面父類 Person 和子類 Employee 都重寫了 equals(),不過 Employee 比父類多了一個id屬性,而且這裏我們並沒有統一語義。測試代碼如下:
  1. public class Test {  
  2.         public static void main(String[] args) {  
  3.             Employee e1 = new Employee("chenssy"23);  
  4.             Employee e2 = new Employee("chenssy"24);  
  5.             Person p1 = new Person("chenssy");  
  6.             System.out.println(p1.equals(e1));  
  7.             System.out.println(p1.equals(e2));  
  8.             System.out.println(e1.equals(e2));  
  9.         }  
  10.     }  
public class Test {
        public static void main(String[] args) {
            Employee e1 = new Employee("chenssy", 23);
            Employee e2 = new Employee("chenssy", 24);
            Person p1 = new Person("chenssy");
            System.out.println(p1.equals(e1));
            System.out.println(p1.equals(e2));
            System.out.println(e1.equals(e2));
        }
    }
上面代碼我們定義了兩個員工和一個普通人,雖然他們同名,但是他們肯定不是同一人,所以按理來說結果應該全部是 false,但是事與願違,結果是:true、true、false。對於那 e1!=e2 我們非常容易理解,因爲他們不僅需要比較 name,還需要比較 ID。但是 p1 即等於 e1 也等於 e2,這是非常奇怪的,因爲 e1、e2 明明是兩個不同的類,但爲什麼會出現這個情況?首先 p1.equals(e1),是調用 p1 的 equals 方法,該方法使用 instanceof 關鍵字來檢查 e1 是否爲 Person 類,這裏我們再看看 instanceof:判斷其左邊對象是否爲其右邊類的實例,也可以用來判斷繼承中的子類的實例是否爲父類的實現。他們兩者存在繼承關係,肯定會返回 true 了,而兩者 name 又相同,所以結果肯定是 true。所以出現上面的情況就是使用了關鍵字 instanceof,這是非常容易導致我們“鑽牛角尖”。故在覆寫 equals 時推薦使用 getClass 進行類型判斷。而不是使用 instanceof(除非子類擁有統一的語義)。

7.編寫一個完美equals()的幾點建議

下面給出編寫一個完美的equals方法的建議(出自Java核心技術 第一卷:基礎知識):

1)顯式參數命名爲otherObject,稍後需要將它轉換成另一個叫做other的變量(參數名命名,強制轉換請參考建議5)

2)檢測this與otherObject是否引用同一個對象 :if(this == otherObject) return true;(存儲地址相同,肯定是同個對象,直接返回true)

3) 檢測otherObject是否爲null ,如果爲null,返回false.if(otherObject == null) return false;

4) 比較this與otherObject是否屬於同一個類 (視需求而選擇)

  • 如果equals的語義在每個子類中有所改變,就使用getClass檢測 :if(getClass()!=otherObject.getClass()) return false; (參考前面分析的第6點)

  • 如果所有的子類都擁有統一的語義,就使用instanceof檢測 :if(!(otherObject instanceof ClassName)) return false;(即前面我們所分析的父類car與子類bigCar混合比,我們統一了批次相同即相等)

5) 將otherObject轉換爲相應的類類型變量:ClassName other = (ClassName) otherObject;

6) 現在開始對所有需要比較的域進行比較 。使用==比較基本類型域,使用equals比較對象域。如果所有的域都匹配,就返回true,否則就返回flase。

  • 如果在子類中重新定義equals,就要在其中包含調用super.equals(other)

  • 當此方法被重寫時,通常有必要重寫 hashCode 方法,以維護 hashCode 方法的常規協定,該協定聲明 相等對象必須具有相等的哈希碼 。


參考資料:
Java核心技術 第一卷:基礎知識

Java深入分析

http://wiki.jikexueyuan.com/project/java-enhancement/java-thirteen.html

發佈了9 篇原創文章 · 獲贊 80 · 訪問量 21萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章