自定義物件作為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介面).