正在看《Thinking in java》學習散列碼相關的知識,在這總結一下吧。
先來看一個例子
class Student{ protected int id;//當前類的成員與繼承該類的類能訪問. public Student(int id) { this.id = id; } @Override public String toString() { return "Student #" + id; }}class Score{ private static Random random = new Random(47); private int score = random.nextInt(100); @Override public String toString() { return String.valueOf(score); }}public class HashCodeTest { public static <T extends Student> void studScore(Class<T> type) throws Exception{ Constructor<T> stud = type.getConstructor(int.class); //利用反射機制來執行個體化及使用Student類和任何從Student派生出來的類 Map<Student, Score> map = new HashMap<>(); for (int i = 0; i < 10; i++) { map.put(stud.newInstance(i), new Score()); } System.out.println("map" + map); Student student = stud.newInstance(3);//使用id為3的Student作為鍵 System.out.println("尋找學生: " + student); if (map.containsKey(student)) { System.out.println(map.get(student)); }else { System.out.println("此學生不存在"); } } public static void main(String[] args) throws Exception{ studScore(Student.class); }}
輸出
map{Student #1=55, Student #4=61, Student #5=29, Student #3=61, Student #8=22, Student #7=0, Student #0=58, Student #2=93, Student #9=7, Student #6=68}尋找學生: Student #3此學生不存在
但是結果它無法找到id為3的鍵。問題出在Student是自動繼承自基類Object,這裡是使用Object的hashCode()方法產生散列碼,預設是使用對象的地址計算散列碼。第一個Student(3)的執行個體和第二個的散列碼是不同的。所以無法找到。
所以需要恰當的覆蓋hashCode()方法。並且同時覆蓋equals方法。因為HashMap使用equals()判斷當前的鍵是否與表中存在的鍵相同
equals()滿足下列條件:
1)自反性 對於任意x, x.equals(x)必定返回true。
2)對稱性 對於任意x,y,x.equals(y)與y.equals(x)的傳回值相同。
3)傳遞性 對於任意x,y,z,如果x.equals(y)與y.equals(z)傳回值相同,那麼x.equals(z)傳回值也相同。
4)一致性 對於任意的x,y,無論x.equals(y)執行多少次,傳回值要麼是true,要麼為false。
5)對於任意x != null, x.equals(null)返回false。
所以要使用自己的類作為HashMap的鍵,必須同時覆蓋hashCode()和equals().
class Student2 extends Student{ public Student2(int id) { super(id); } @Override public int hashCode() { return id;//返回id作為散列碼 } @Override public boolean equals(Object obj) { return obj instanceof Student2 && (id == ((Student2)obj).id); //instanceof檢查了此對象是否為null,然後基於每個對象中實際的id進行比較 }}public class HashCode2 { public static void main(String[] args) throws Exception { HashCodeTest.studScore(Student2.class); }}
輸出
map{Student #0=58, Student #1=55, Student #2=93, Student #3=61, Student #4=61, Student #5=29, Student #6=68, Student #7=0, Student #8=22, Student #9=7}尋找學生: Student #361
為速度而散列
線性查詢是最慢的查詢方法
散列將鍵儲存在某處,以便能夠很快找到。儲存一組元素最快的資料結構是數組,所以使用它來表示鍵的資訊。
數組並不儲存鍵本身,而是通過鍵對象產生一個數字,將其作為數組的下標,這個數字就是散列碼。
衝突有外部連結處理,數組並不直接儲存值,而是儲存值得list。然後對list中的值使用equals()方法進行線性查詢。(這部分的查詢會比較慢)
散列表的“槽位”(slot)通常被稱為桶位(bucket),Java的散列函數使用2的整數次方(求餘和除法是最慢的操作,使用2的整數次方長度的散列碼,可以用掩碼代替除法,可以減少get()中%操作的開銷)。
覆蓋hashCode()
無論何時,對同一個對象調用hashCode()都應該產生同樣的值。且不能依賴易變的資料。
也不應該使hashCode()依賴於唯一性的對象資訊。
以String類為例,如果程式中有多個String對象,都包含相同的字串序列,那麼這些String對象都映射到同一塊記憶體地區。
public class StringTest { public static void main(String[] args) { String str1 = new String("hello"); String str2 = new String("hello"); System.out.println(str1.hashCode()); System.out.println(str2.hashCode()); System.out.println(str1.equals(str2)); System.out.println(str1 == str2); //雙等號就是比較的棧裡面的內容,未經處理資料類型和地址都是放在棧裡面的。而equals則是根據地址拿到堆裡面的內容進行比較。 }}
輸出
9916232299162322truefalse
對String而言,hashCode()明顯是基於String的內容的。根據對象的內容產生散列碼,散列碼不一定是獨一無二,但是通過hashCod()和equals()必須能夠完全確定對象的身份
來看看String的源碼
public int hashCode() { int h = hash; if (h == 0 && value.length > 0) { char val[] = value; for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; } hash = h; } return h; } //String類中的hashCode計算方法還是比較簡單的,就是以31為權,每一位為字元的ASCII值進行運算.public boolean equals(Object anObject) { if (this == anObject) { return true; } if (anObject instanceof String) { String anotherString = (String)anObject; int n = value.length; if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0; while (n-- != 0) { if (v1[i] != v2[i]) return false; i++; } return true; } } return false; }