1. 程式人生 > >Java中hashCode與equals方法的約定及重寫原則

Java中hashCode與equals方法的約定及重寫原則

Java中Set的contains()方法 —— hashCode與equals方法的約定及重寫原則

翻譯人員: 鐵錨
翻譯時間: 2013年11月5日
原文連結: Java hashCode() and equals() Contract for the contains(Object o) Method of Set

本文主要討論 集合Set 中儲存物件的 hashCode 與 equals 方法應遵循的約束關係.

新手對Set中contains()方法的疑惑
import java.util.HashSet;
 
class Dog{
	String color;
 
	public Dog(String s){
		color = s;
	}	
}
 
public class SetAndHashCode {
	public static void main(String[] args) {
		HashSet<Dog> dogSet = new HashSet<Dog>();
		dogSet.add(new Dog("white"));
		dogSet.add(new Dog("white"));
 
		System.out.println("We have " + dogSet.size() + " white dogs!");
 
		if(dogSet.contains(new Dog("white"))){
			System.out.println("We have a white dog!");
		}else{
			System.out.println("No white dog!");
		}	
	}
}

上述程式碼的輸出為:
We have 2 white dogs!
No white dog!

程式中添加了兩隻白色的小狗到集合dogSet中. 且 size()方法顯示有2只白色的小狗.但為什麼用 contains()方法來判斷時卻提示沒有白色的小狗呢?

Set的contains(Object o) 方法詳解
Java的API文件指出: 當且僅當 本set包含一個元素 e,並且滿足(o==null ? e==null : o.equals(e))條件時,contains()方法才返回true. 因此 contains()方法 必定使用equals方法來檢查是否相等.
需要注意的是: set 中是可以包含 null值的(常見的集合類都可以包含null值). 所以如果添加了null,然後判斷是否包含null,將會返回true,程式碼如下所示:
HashSet<Dog> a = new HashSet<Dog>();
a.add(null);
if(a.contains(null)){
	System.out.println("true");
}

Java的根類Object定義了  public boolean equals(Object obj) 方法.因此所有的物件,包括陣列(array,[]),都實現了此方法。
在自定義類裡,如果沒有明確地重寫(override)此方法,那麼就會使用Object類的預設實現.即只有兩個物件(引用)指向同一塊記憶體地址(即同一個實際物件, x==y為true)時,才會返回true。
如果把Dog類修改為如下程式碼,能實現我們的目標嗎?
class Dog{
	String color;
 
	public Dog(String s){
		color = s;
	}
 
	//重寫equals方法, 最佳實踐就是如下這種判斷順序:
	public boolean equals(Object obj) {
		if (!(obj instanceof Dog))
			return false;	
		if (obj == this)
			return true;
		return this.color == ((Dog) obj).color;
	}
 
}
英文答案是: no.

問題的關鍵在於 Java中hashCode與equals方法的緊密聯絡. hashCode() 是Object類定義的另一個基礎方法.

equals()與hashCode()方法之間的設計實現原則為:
如果兩個物件相等(使用equals()方法),那麼必須擁有相同的雜湊碼(使用hashCode()方法).
即使兩個物件有相同的雜湊值(hash code),他們不一定相等.意思就是: 多個不同的物件,可以返回同一個hash值.

hashCode()的預設實現是為不同的物件返回不同的整數.有一個設計原則是,hashCode對於同一個物件,不管內部怎麼改變,應該都返回相同的整數值.
在上面的例子中,因為未定義自己的hashCode()實現,因此預設實現對兩個物件返回兩個不同的整數,這種情況破壞了約定原則。

解決辦法
class Dog{
	String color;
 
	public Dog(String s){
		color = s;
	}
 
	//重寫equals方法, 最佳實踐就是如下這種判斷順序:
	public boolean equals(Object obj) {
		if (!(obj instanceof Dog))
			return false;	
		if (obj == this)
			return true;
		return this.color == ((Dog) obj).color;
	}
 
	public int hashCode(){
		return color.length();//簡單原則
	}
}


但是上面的hashCode實現,要求Dog的color是不變的.否則會出現如下的這種困惑:
import java.util.HashSet;
import java.util.Set;


public class TestContains {


	public static final class Person{
		private String name = "";
		public Person(String n) {
			setName(n);
		}
		public String getName() {
			return name;
		}
		public void setName(String name) {
			this.name = (name==null)? "" : name;
		}
		@Override
		public int hashCode() {
			// 請考慮是否值得這麼做,因為此時name是會變的.
			return name.length();
			// 推薦讓name不可改變
		}
		@Override
		public boolean equals(Object obj) {
			if(!(obj instanceof Person)){
				return false;
			}
			if(obj == this){
				return true;
			}
			return this.name.equals(((Person)obj).name);
		}
	};
	
	public static void main(String[] args) {
		Set<Person> persons = new HashSet<Person>();
		//
		Person person = new Person("tiemao");
		persons.add(person);
		// 修改name, 則依賴hash的集合可能失去作用
		person.setName("ren");
		// 同一個物件,居然是false,原因是我們重寫了hashCode,打破了hashCode不變的基本約定
		boolean has = persons.contains(person);
		int size = persons.size();
		System.out.println("has="+has);	// has=false.
		System.out.println("size="+size);// size=1
	}
}

參考文章: 
http://docs.oracle.com/javase/6/docs/api/java/lang/Object.html

相關閱讀
4. 理解Java機制最受歡迎的8幅圖