Java入門系列之hashCode和equals(十二)
前言
前面兩節內容我們詳細講解了Hashtable演算法和原始碼分析,針對雜湊函式始終逃脫不掉hashCode的計算,本節我們將詳細分析hashCode和equals,同時您將會看到本節內容是從《Effective Java》學習整理而來(吐槽一句,這本書中文版翻譯的真垃圾),對於《Effective Java》這本書很有學習價值,但是我不會像其他童鞋一樣,直接從這本書講解一個系列,我所採用的是學習到對應地方然後參考不同java經典書籍進行總結,循序漸進式這樣效果更佳,好了,我們開始吧。
equals
翻看《Effective Java》關於equals這一節內容,直接丟擲重寫equals必須遵守的如下五大約定,當我看到這幾大特性時,頓時驚呆了,這不就是大學線代講解矩陣時的特點麼,學以致用原來是這麼個道理。
1、自反性:對於非空的物件x,x.equals(x)必須返回true.
2、對稱性:對於非空的物件x和y,若x.equals(y)等於true時,那麼y.equals(x)也必須返回true.
3、傳遞性:對於非空的物件x、y和z,如果x.equals(y)和y.equals(z)等於true時,那麼x.equals(z)也必須返回true
4、一致性:對於非空的物件x和y,如果利用equals判斷物件的資訊沒有被修改時,無論呼叫多少次,那麼x.equals(y)要麼為true,要麼為false
5、對於非空的物件x,x.equals(null)必須返回false
關於第一點很好理解,非空物件自身引用必須相等,對於第二點書中所給的例子則是將重寫物件比較某個字串時不區分大小寫,但是字串物件是區分大小寫,如此這樣將導致對稱不一致問題,對於第三點則是繼承時注意equals的傳遞性,第4點則強調多次呼叫通過equals判斷的恆等性,最後一點更好理解如若不判斷則會丟擲空指標異常。那麼我們實際在重寫equals時可將以下幾點作為模板來使用就可以啦。
1、使用“==”判斷兩個物件是否引用相同
2、使用instanceof操作符來檢查引數型別是否相同
3、若型別相同,則將引數轉換為正確的型別
4、比較物件中每個值是否都相等,若全部相等則返回true,否則為false
如上幾點模板來自《Effective Java》對重寫equals的總結,當然我們可以從重寫字串物件中的equals找到如上影子,字串物件的equals方法如下:
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; }
好了,到這裡我們講解完了equals,還是比較簡單,那麼重寫equals時為何一定要重寫hashCode呢?主要原因在於:這是通用約定,如果是基於雜湊的集合比較HashMap或者HashSet等,儲存物件地址需要通過雜湊函式計算hashCode,如若不這樣做將會出現意想不到的問題。那麼意想不到的問題是什麼呢?
hashCode
下面我們用一個例子來講解為何重寫equals時一定要重寫hashCode。
public class Person { int age; String name; public Person(int age, String name) { this.age = age; this.name = name; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj instanceof Person) { Person p = (Person) obj; return (this.age == p.age && this.name == p.name); } return false; } }
如上我們給出一個Person物件,然後帶有年齡和名稱兩個屬性,重寫時判斷年齡和名稱相等即可認為為同一人,下面我們在控制檯進行如下操作,然後我們看看將會打印出什麼結果呢。
Person p1 = new Person(12, "Jeffcky"); Person p2 = new Person(12, "Jeffcky"); Hashtable hashtable = new Hashtable(); hashtable.put(p1, "v1"); System.out.println(hashtable.get(p2));
不難理解,因為Hashtable物件儲存地址是基於hashCode,但是上述我們沒有重寫hashCode,所以我們例項化物件p2時,即使重寫了equals兩個物件相等,結果獲取p2的值肯定是獲取不到的,因為hashCode不等,接下來我們重寫hashCode
@Override public int hashCode() { return (31 * Integer.valueOf(this.age).hashCode() + name.hashCode()); }
我們看到字串物件重寫了hashCode,因為字串用的很頻繁,同時我們極有可能在雜湊集合中用到。下面我們來看看字串物件的hashCode實現方式。
上圖示記出的就是計算字串的hashCode核心即雜湊函式,從上看出通過字串中每一個字元的ASCII碼來計算,同時我們也可再拓展下看原始碼數值型別的hashCode就是其本身。上述計算方式最終我們數學進行歸納出計算方法為:
s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
比如我們計算字串【AC】的hashCode,根據如上計算公式則是
65*31^(2-1) + 67*31^(2-2) = 2082
在《Effective Java》中提到之所以選擇31的原因是:它是一個奇素數,如果乘數是偶數,並且乘法溢位的話,資訊就會丟失,因為2相乘等價於移位運算。使用素數的好處並不很明顯,但是習慣使用素數來計算雜湊結果。我嚴重懷疑是不是翻譯的人理解錯了意思,對於書中給出選擇素數的原因無法讓人折服,這裡我來講解我個人的想法。
雜湊函式為什麼要使用質數
選擇31的原因是因為它是質數(素數),而不是因為它是奇數。當我們插入一個元素到雜湊表中時,雜湊如何識別需要將元素儲存在哪個儲存桶中(Bucket)呢?這是一個重要的問題,使得強制性要求雜湊能夠在恆定時間內告訴我們將值儲存在哪個儲存桶中,以便能夠快速檢索。我們能想到的是傻瓜式操作方式即迴圈遍歷比較,這種順序搜尋將直接導致雜湊效能惡化,直接取決雜湊表所包含值的數量。換句話說,這將具有線性效能成本(O(N)),隨著鍵(N)的數量越來越大,效能可想而知。另一個複雜之處是我們要處理的值的實際型別。若我們要處理字串和其他複雜型別,檢查或比較本身的數量將導致成本又將變得很高。基於以上敘述,所以我們至少需要解決兩個問題,其一是便於快速檢索而非順序檢索,其二是解決複雜型別值的比較。解決此問題的簡單方法是希望出現一種將複雜值分解為易於使用的鍵或雜湊的方法,實現此過程的最簡單方法是生成唯一編號,該數字必須是唯一的,因為我們要區分一個值和另一個值。質數是唯一數字,它們的獨特之處在於,由於使用了素數來構成素數,因此素數與任何其他數字的乘積具有的最大可能的唯一性(不畫素數本身那樣唯一),質數的此屬性在雜湊函式中使用可減少衝突次數(或碰撞)。例如使用4 * 8,則它比諸如3 * 5的質數乘積更有可能發生衝突,32可以通過1 * 32或2 * 16或4 * 8或2 ^ 5等計算得到,但3*5 只能以1 * 15或3 * 5得到15。
總結
本文我們詳細討論了hashCode和equals,以及分析了在雜湊函式中使用質數的原因,這裡還存在一節內容留到學習虛擬機器時再補上,通過分析虛擬機器原始碼瞭解hashCode具體實現,下一節我們將進入學習分析HashMap原始碼,感謝您的閱讀,我們下節