1. 程式人生 > >自定義物件作為HashMap的Key

自定義物件作為HashMap的Key

這個問題在很多面試者面試時都會被提及,本人也是最近在看effective java第九條:覆蓋equals時總要覆蓋hashcode回想到了當初面試時也被問到了這個問題.於是動手寫了幾行程式碼,還真發現了一些小的問題,所以拿出來分享一下!

首先我們自定義一個學生物件,它有姓名和年齡兩個欄位.

class Student{
	public String name;
	public Integer age;
	
	Student(String name,Integer age){
		this.name = name;
		this.age = age;
	}
	
	@Override
	public boolean equals(Object o) {
		//return true;
		if(o==this)
			return true;
			if(!(o instanceof Student))
				return false;
			Student s = (Student)o;
		return s.name.equals(name)&&s.age.equals(age);
	}
	
	@Override
	public int hashCode() {
		int result = 17;
		result = 31 * result + name.hashCode();
		result = 31 * result + age;
		return result;
	}
}

(PS)上面的程式碼是一個能作為hashMap的key物件的完整程式碼.包括重寫了equals方法和hashCode方法.在重寫equals方法時我還遇到了一個麻煩事,一開始我是下面這樣寫的:

@Override
	public boolean equals(Object o) {
		//***********
		return s.name.equals(name)&&s.age==age;
	}

相信都能找到原因,age是Integer物件而不是int所以比較的是地址值,於是乎無論如何都不能得到我想要的結果.

然後我們接著把物件裝入HashMap結構中,並取出,看是否能夠成功?

static void demo2(){
		Map<Student, String> map = new HashMap<Student, String>();		
		long l1 = System.currentTimeMillis();
		for(int i = 0;i<10000;i++){
			map.put(new Student("dy"+i, i), ""+i);
		}
		long l2 = System.currentTimeMillis();
		System.out.println(map.get(new Student("dy9999",9999)));
		long l3 = System.currentTimeMillis();
		System.out.println((l2-l1));
		System.out.println((l3-l2));
	}

結果如下:

9999
8
0

已經成功了!

那麼可能有點新的問題了!那就是Student物件的hashCode方法是怎麼實現的呢?equals方法大家都會重寫.那麼究竟怎麼一個演算法能讓不同的物件具有不同的雜湊值呢?下面這段描述摘抄自effective java給我們的建議:

1.把某個非零的常數值,比如說17(一個你喜歡的數字),儲存在一個名為result的int型別的變數中.

2.對於物件中每個關鍵域(指equals方法中涉及的每個域),完成以下步驟:

      a.為該域計算int型別的雜湊碼c:

         i.如果該域是boolean型別,則計算(f?1:0)

         ii.如果該域是byte,char,short或者int型別,則計算(int)f.

         iii.如果該域是long型別,則計算(int)(f^(f>>>32)).

         iv.如果該域是float型別,則計算Float.floatToIntBits(f).

         v.如果該域是double型別,則計算Double.doubleToLongBits(f),然後按照步驟2.a.iii,為得到的long型別值計算雜湊值.

         vi.如果該域是一個物件引用,並且該類的equals方法通過遞迴地呼叫equals的方式來比較這個域,則同樣為這個域遞迴地呼叫hashCode.如果需要更加複雜的比較,則為這個域計算一個"正規化",然後針對這個正規化呼叫hashCode.如果這個域的值為null,則返回0(或者其他某個常數,但通常是0).

         vii.如果該域是一個數組,則要把每一個元素當做單獨的域來處理.也就是說,遞迴地應用上述規則,對每個重要的元素計算一個雜湊碼,然後根據步驟2.b中的做法把這些雜湊值組合起來.如果陣列域中的每個元素都很重要,可以利用發行版本1.5中增加的其中一個Arrays.hashCode方法.

     b.按照下面的公式,把步驟2.a中計算得到的雜湊碼c合併到result中:

     result = 31 * result +c;

3.返回result

當然如果我們不重寫hashCode方法會出現什麼情況呢?請看:

null
8
0

返回結果為null,因為Student類沒有重寫hashCode方法,從而導致兩個相等的例項具有不相等的雜湊碼,違反了hashCode的約定.因此put方法把物件放在一個雜湊桶中,而get方法卻在另一個雜湊桶中取值.即使這兩個例項恰好被放在同一個雜湊桶中,get方法也必定會返回null,因為HashMap有一項優化,可以將與每個相關聯的雜湊碼快取起來,如果雜湊碼不匹配,也不必檢查物件的等同性!這正說明了effective java第九條:覆蓋equals方法時總要覆蓋hashCode.但是現在又有一個問題了,如果我重寫的hashCode程式碼如下會如何呢?

	@Override
	public int hashCode() {
		/*int result = 17;
		result = 31 * result + name.hashCode();
		result = 31 * result + age;*/
		return 32;
	}

執行的結果如下:

9999
2305
1

可以看到的是,由於每個物件都具有相同的雜湊值,因此,每個物件都被對映到同一個雜湊桶中,使散列表退化為連結串列,它使得本該線性時間執行的程式變成了以平方級時間在執行.

關於物件實現Compareable介面可以參考這篇文章(Java 8 HashMap鍵與Comparable介面).