Java中的equals方法和hashCode方法是Object中的,所以每個對象都是有這兩個方法的,有時候我們需要實現特定需求,可能要重寫這兩個方法,今天就來介紹一些這兩個方法的作用。
equals()和hashCode()方法是用來在同一類中做比較用的,尤其是在容器裡如set存放同一類對象時用來判斷放入的對象是否重複。
這裡我們首先要明白一個問題:
equals()相等的兩個對象,hashcode()一定相等,equals()不相等的兩個對象,卻並不能證明他們的hashcode()不相等。換句話說,equals()方法不相等的兩個對象,hashCode()有可能相等。(我的理解是由於雜湊碼在產生的時候產生衝突造成的)
在這裡hashCode就好比字典裡每個字的索引,equals()好比比較的是字典裡同一個字下的不同詞語。就好像在字典裡查“自”這個字下的兩個詞語“自己”、“自發”,如果用equals()判斷查詢的詞語相等那麼就是同一個詞語,比如equals()比較的兩個詞語都是“自己”,那麼此時hashCode()方法得到的值也肯定相等;如果用equals()方法比較的是“自己”和“自發”這兩個詞語,那麼得到結果是不想等,但是這兩個詞都屬於“自”這個字下的詞語所以在查索引時相同,即:hashCode()相同。如果用equals()比較的是“自己”和“他們”這兩個詞語的話那麼得到的結果也是不同的,此時hashCode() 得到也是不同的。
反過來:hashcode()不等,一定能推出equals()也不等;hashcode()相等,equals()可能相等,也可能不等。在object類中,hashcode()方法是本地方法,返回的是對象的地址值:
public native int hashCode();
而object類中的equals()方法比較的也是兩個對象的地址值,如果equals()相等,說明兩個對象地址值也相等,當然hashcode()也就相等了。 equals()與==
1)對於== 如果作用於基礎資料型別 (Elementary Data Type)的變數,則直接比較其儲存的 “值”是否相等 如果作用於參考型別的變數,則比較的是所指向的對象的地址
2)對於equals方法(注意:equals方法不能作用於基礎資料型別 (Elementary Data Type)的變數) 如果沒有對equals方法進行重寫,則比較的是參考型別的變數所指向的對象的地址,諸如String、Date等類對equals方法進行了重寫的話,比較的是所指向的對象的內容
其主要的不同是一個是操作符一個是方法,==用於對比原生類型而equals()方法比較對象的相等性。 hashCode()函數
Java 裡面當你重寫 equals 的時候需要同時重寫 hashCode。但是如何?一個 hashCode? 返回一個固定值。
@Overridepublic int hashCode() { return 42;}
42 可是生命、宇宙以及任何事情的終極答案。 來當一個對象的hashCode 是綽綽有餘了。不過雖然返回固定值是合法的行為。但是是最不推薦的一種的做法,因為這樣會導致當該對象被用作 HashMap 的 Key 的時候不斷的衝突。get 的複雜度會退化為 O(n).所以 42 雖然是生命、宇宙以及任何事情的終極答案, 但是計算42的真實含義需要地球運算數萬年才能得出。普通的電腦還是不能這樣隨便用的。 傳統的實現方式
傳統實現一個hashCode 需要三步 取一個非0的 int 作為初始值,比如 42,儲存在 result 中 計算你所關心的域(在equals起作用的)的 hashCode。如何產生屬性的 hashCode 我們等一下再說,先假設產生的 hashCode 為 c 組合所有的域,result = 31 * result + c Java 7+ 實現風格
Objects 是 Java 7 新加的一個類。其中有一個計算 hash 的方法:
public static int hash(Object... values)
所以計算 HashCode 只需要用下面的代碼:
@Override public int hashCode() { reutrn Objects.hash(p1, p2, p3, p4);}如何計算各個屬性的 hash 值
這一部分我覺得用代碼來說明是最簡單的,代碼由 IntelliJ 自動產生。主要是各個原始類型的建置規則。
public class TestClass { byte b; char c; short s; int i; long l; float f; double d; String string; @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; TestClass testClass = (TestClass) o; if (b != testClass.b) return false; if (c != testClass.c) return false; if (s != testClass.s) return false; if (i != testClass.i) return false; if (l != testClass.l) return false; if (Float.compare(testClass.f, f) != 0) return false; if (Double.compare(testClass.d, d) != 0) return false; return string != null ? string.equals(testClass.string) : testClass.string == null; } @Override public int hashCode() { int result; long temp; result = (int) b; result = 31 * result + (int) c; result = 31 * result + (int) s; result = 31 * result + i; result = 31 * result + (int) (l ^ (l >>> 32)); result = 31 * result + (f != +0.0f ? Float.floatToIntBits(f) : 0); temp = Double.doubleToLongBits(d); result = 31 * result + (int) (temp ^ (temp >>> 32)); result = 31 * result + (string != null ? string.hashCode() : 0); return result; }}
可以看看JDK內部,對hashCode()的實現,Arrays就是一個很好的例子:
public static int hashCode(boolean a[]) { if (a == null) return 0; int result = 1; for (boolean element : a) result = 31 * result + (element ? 1231 : 1237); return result; }
public static int hashCode(long a[]) { if (a == null) return 0; int result = 1; for (long element : a) { int elementHash = (int)(element ^ (element >>> 32)); result = 31 * result + elementHash; } return result; }
//int、short、char、bytepublic static int hashCode(int a[]) { if (a == null) return 0; int result = 1; for (int element : a) result = 31 * result + element; return result; }
看到這裡那你也許會有和我一樣的疑惑,為什麼是31? 為什麼 hashCode 方法選擇數字31作為乘子
這個數字居然不是用常量聲明的,所以沒法從字面意思上推斷這個數位用途。後來帶著疑問和好奇心,到網上去找資料查詢一下。在看完資料後,默默的感歎了一句,原來是這樣啊。那麼到底是哪樣呢。在接下來章節裡,請大家帶著好奇心和我揭開數字31的用途之謎。
在詳細說明 String hashCode 方法選擇數字31的作為乘子的原因之前,我們先來看看 String hashCode 方法是怎樣實現的,如下:
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 方法的實現,是不是很簡單。實際上 hashCode 方法核心的計算邏輯只有三行,也就是代碼中的 for 迴圈。我們可以由上面的 for 迴圈推匯出一個計算公式,hashCode 方法注釋中已經給出。如下:
s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
這裡說明一下,上面的 s 數組即源碼中的 val 數組,是 String 內部維護的一個 char 類型數組。這裡我來簡單推導一下這個公式:
假設 n=3i=0 -> h = 31 * 0 + val[0]i=1 -> h = 31 * (31 * 0 + val[0]) + val[1]i=2 -> h = 31 * (31 * (31 * 0 + val[0]) + val[1]) + val[2] h = 31*31*31*0 + 31*31*val[0] + 31*val[1] + val[2] h = 31^(n-1)*val[0] + 31^(n-2)*val[1] + val[2]
上面的公式包括公式的推導並不是本文的重點,大家瞭解瞭解即可。接下來來說說本文的重點,即選擇31的理由。從網上的資料來看,一般有如下兩個原因:
第一,31是一個不大不小的質數,是作為 hashCode 乘子的優選質數之一。另外一些相近的質數,比如37、41、43等等,也都是不錯的選擇。那麼為啥偏偏選中了31呢。請看第二個原因。
第二、31可以被 JVM 最佳化,31 * i = (i << 5) - i。
上面兩個原因中,第一個需要解釋一下,第二個比較簡單,就不說了。下面我來解釋第一個理由。一般在設計雜湊演算法時,會選擇一個特殊的質數。至於為啥選擇質數,我想應該是可以降低雜湊演算法的衝突率。至於原因,這個就要問數學家了,我幾乎可以忽略的數學水平解釋不了這個原因。上面說到,31是一個不大不小的質數,是優選乘子。那為啥同是質數的2和101(或者更大的質數)就不是優選乘子呢,分析如下。
這裡先分析質數2。首先,假設n = 6,然後把質數2和n帶入上面的計算公式。並僅計算公式中次數最高的那一項,結果是2^5 = 32,是不是很小。所以這裡可以斷定,當字串長度不是很長時,用質數2做為乘子算出的雜湊值,數值不會很大。也就是說,雜湊值會分布在一個較小的數值區間內,分布性不佳,最終可能會導致衝突率上升。
上面說了,質數2做為乘子會導致雜湊值分布在一個較小區間內,那麼如果用一個較大的大質數101會產生什麼樣的結果呢。根據上面的分析,我想大家應該可以猜出結果了。就是不用再擔心雜湊值會分布在一個小的區間內了,因為101^5 = 10,510,100,501。但是要注意的是,這個計算結果太大了。如果用 int 類型表示雜湊值,結果會溢出,最終導致數值資訊丟失。儘管數值資訊丟失並不一定會導致衝突率上升,但是我們暫且先認為質數101(或者更大的質數)也不是很好的選擇。最後,我們再來看看質數31的計算結果:31^5 = 28629151,結果值相對於32和10,510,100,501來說。是不是很nice,不大不小。
上面用了比較簡陋的數學手段證明了數字31是一個不大不小的質數,是作為 hashCode 乘子的優選質數之一。接下來我會用詳細的實驗來驗證上面的結論,不過在驗證前,我們先看看 Stack Overflow 上關於這個問題的討論,Why does Java’s hashCode() in String use 31 as a multiplier?。其中排名第一的答案引用了《Effective Java》中的一段話,這裡也引用一下:
The value 31 was chosen because it is an odd prime. If it were even and the multiplication overflowed, information would be lost, as multiplication by 2 is equivalent to shifting. The advantage of using a prime is less clear, but it is traditional. A nice property of 31 is that the multiplication can be replaced by a shift and a subtraction for better performance: 31 * i == (i << 5) - i. Modern VMs do this sort of optimization automatically.
翻譯一下:
選擇數字31是因為它是一個奇質數,如果選擇一個偶數會在乘法運算中產生溢出,導致數值資訊丟失,因為乘二相當於移位元運算。選擇質數的優勢並不是特別的明顯,但這是一個傳統。同時,數字31有一個很好的特性,即乘法運算可以被移位和減法運算取代,來擷取更好的效能:31 * i == (i << 5) - i,現代的 JAVA 虛擬機器可以自動的完成這個最佳化。
排名第二的答案設這樣說的:
As Goodrich and Tamassia point out, If you take over 50,000 English words (formed as the union of the word lists provided in two variants of Unix), using the constants 31, 33, 37, 39, and 41 will produce less than 7 collisions in each case. Knowing this, it should come as no surprise that many Java implementations choose one of these constants.
翻譯一下:
正如 Goodrich 和 Tamassia 指出的那樣,如果你對超過50,000個英文單詞(由兩個不同版本的 Unix 字典合并而成)進行 hash code 運算,並使用常數 31, 33, 37, 39 和 41 作為乘子,每個常數算出的雜湊值衝突數都小於7個,所以在上面幾個常數中,常數 31 被 Java 實現所選用也就不足為奇了。
上面的兩個答案完美的解釋了 Java 源碼中選用數字31的原因。第二個答案的驗證,感興趣的朋友可以看看我所參考的原文:https://segmentfault.com/a/1190000010799123
一篇非常乾貨的文章~
參考連結:
https://segmentfault.com/a/1190000010799123
http://www.jianshu.com/p/039c942b22c0
http://blog.csdn.net/jiangwei0910410003/article/details/22739953
JDK 8 源碼