1. 程式人生 > >java複習(6)—HashCode與equals方法的關係

java複習(6)—HashCode與equals方法的關係

一、HashCode的作用

首先,想要明白hashCode的作用,必須要先知道Java中的集合。  

總的來說,Java中的集合(Collection)有兩類,一類是List,再有一類是Set。 前者集合內的元素是有序的,元素可以重複;後者元素無序,但元素不可重複。

那麼這裡就有一個比較嚴重的問題了:要想保證元素不重複,可兩個元素是否重複應該依據什麼來判斷呢? 這就是Object.equals方法了。但是,如果每增加一個元素就檢查一次,那麼當元素很多時,後新增到集合中的元素比較的次數就非常多了。 也就是說,如果集合中現在已經有1000個元素,那麼第1001個元素加入集合時,它就要呼叫1000次equals方法。這顯然會大大降低效率。  

於是,Java採用了雜湊表的原理。雜湊(Hash)實際上是個人名,由於他提出一雜湊演算法的概念,所以就以他的名字命名了。 雜湊演算法也稱為雜湊演算法,是將資料依特定演算法直接指定到一個地址上。初學者可以這樣理解,hashCode方法實際上返回的就是物件儲存的實體地址(實際可能並不是)。   

這樣一來,當集合要新增新的元素時,先呼叫這個元素的hashCode方法,就一下子能定位到它應該放置的物理位置上。 如果這個位置上沒有元素,它就可以直接儲存在這個位置上,不用再進行任何比較了;如果這個位置上已經有元素了, 就呼叫它的equals方法與新元素進行比較,相同的話就不存了,不相同就雜湊其它的地址。 所以這裡存在一個衝突解決的問題。這樣一來實際呼叫equals方法的次數就大大降低了,幾乎只需要一兩次。 

所以,Java對於eqauls方法和hashCode方法是這樣規定的:

1、如果兩個物件相同,那麼它們的hashCode值一定要相同;

2、如果兩個物件的hashCode相同,它們並不一定相同。(上面說的物件相同指的是用eqauls方法比較)  

你當然可以不按要求去做了,但你會發現,相同的物件可以出現在Set集合中。同時,增加新元素的效率會大大下降。

hashcode這個方法是用來鑑定2個物件是否相等的。 那你會說,不是還有equals這個方法嗎? 不錯,這2個方法都是用來判斷2個物件是否相等的。但是他們是有區別的。 一般來講,equals這個方法是給使用者呼叫的,如果你想判斷2個物件是否相等,你可以重寫equals方法,然後在程式碼中呼叫,就可以判斷他們是否相等 了。簡單來講,equals方法主要是用來判斷從表面上看或者從內容上看,2個物件是不是相等。

舉個例子,有個學生類,屬性只有姓名和性別,那麼我們可以 認為只要姓名和性別相等,那麼就說這2個物件是相等的。 hashcode方法一般使用者不會去呼叫,比如在hashmap中,由於key是不可以重複的,他在判斷key是不是重複的時候就判斷了hashcode 這個方法,而且也用到了equals方法。這裡不可以重複是說equals和hashcode只要有一個不等就可以了!所以簡單來講,hashcode相 當於是一個物件的編碼,就好像檔案中的md5,他和equals不同就在於他返回的是int型的,比較起來不直觀。我們一般在覆蓋equals的同時也要 覆蓋hashcode,讓他們的邏輯一致。舉個例子,還是剛剛的例子,如果姓名和性別相等就算2個物件相等的話,那麼hashcode的方法也要返回姓名 的hashcode值加上性別的hashcode值,這樣從邏輯上,他們就一致了。 要從物理上判斷2個物件是否相等,用==就可以了。

在Java語言中,equals()和hashCode()兩個函式的使用是緊密配合的,你要是自己設計其中一個,就要設計另外一個。在多數情況 下,這兩個函式是不用考慮的,直接使用它們的預設設計就可以了。但是在一些情況下,這兩個函式最好是自己設計,才能確保整個程式的正常執行。最常見的是當 一個物件被加入收集物件(collection object)時,這兩個函式必須自己設計。更細化的定義是:如果你想將一個物件A放入另一個收集物件B裡,或者使用這個物件A為查詢一個元物件在收集對 象B裡位置的鑰匙,並支援是否容納,刪除收集物件B裡的元物件這樣的操作,那麼,equals()和hashCode()函式必須開發者自己定義。其他情 況下,這兩個函式是不需要定義的。

equals():

它是用於進行兩個物件的比較的,是物件內容的比較,當然也能用於進行物件參閱值的比較。什麼是物件參閱值的比較?就是兩個參閱變數的值得比較,我們 都知道參閱變數的值其實就是一個數字,這個數字可以看成是鑑別不同物件的代號。兩個物件參閱值的比較,就是兩個數字的比較,兩個代號的比較。這種比較是默 認的物件比較方式,在Object這個物件中,這種方式就已經設計好了。所以你也不用自己來重寫,浪費不必要的時間。

物件內容的比較才是設計equals()的真正目的,Java語言對equals()的要求如下,這些要求是必須遵循的。否則,你就不該浪費時間:

•對稱性:如果x.equals(y)返回是“true”,那麼y.equals(x)也應該返回是“true”。

•反射性:x.equals(x)必須返回是“true”。

•類推性:如果x.equals(y)返回是“true”,而且y.equals(z)返回是“true”,那麼z.equals(x)也應該返回是“true”。

•還有一致性:如果x.equals(y)返回是“true”,只要x和y內容一直不變,不管你重複x.equals(y)多少次,返回都是“true”。

•任何情況下,x.equals(null),永遠返回是“false”;x.equals(和x不同型別的物件)永遠返回是“false”。

hashCode():
這個函式返回的就是一個用來進行赫希操作的整型代號,請不要把這個代號和前面所說的參閱變數所代表的代號弄混了。後者不僅僅是個代號還具有在記憶體中才查詢對 象的位置的功能。hashCode()所返回的值是用來分類物件在一些特定的收集物件中的位置。這些物件是HashMap, Hashtable, HashSet,等等。這個函式和上面的equals()函式必須自己設計,用來協助HashMap, Hashtable, HashSet,等等對自己所收集的大量物件進行搜尋和定位。

這些收集物件究竟如何工作的,想象每個元物件hashCode是一個箱子的 編碼,按照編碼,每個元物件就是根據hashCode()提供的代號歸入相應的箱子裡。所有的箱子加起來就是一個HashSet,HashMap,或 Hashtable物件,我們需要尋找一個元物件時,先看它的程式碼,就是hashCode()返回的整型值,這樣我們找到它所在的箱子,然後在箱子裡,每 個元物件都拿出來一個個和我們要找的物件進行對比,如果兩個物件的內容相等,我們的搜尋也就結束。這種操作需要兩個重要的資訊,一是物件的 hashCode(),還有一個是物件內容對比的結果。

hashCode()的返回值和equals()的關係如下:

•如果x.equals(y)返回“true”,那麼x和y的hashCode()必須相等。

•如果x.equals(y)返回“false”,那麼x和y的hashCode()有可能相等,也有可能不等。

為什麼這兩個規則是這樣的,原因其實很簡單,拿HashSet來說吧,HashSet可以擁有一個或更多的箱子,在同一個箱子中可以有一個 或更多的獨特元物件(HashSet所容納的必須是獨特的元物件)。這個例子說明一個元物件可以和其他不同的元物件擁有相同的hashCode。但是一個 元物件只能和擁有同樣內容的元物件相等。所以這兩個規則必須成立。

設計這兩個函式所要注意到的:
如果你設計的物件型別並不使用於收集性物件,那麼沒有必要自己再設計這兩個函式的處理方式。這是正確的面向物件設計方法,任何使用者一時用不到的功能,就先不要設計,以免給日後功能擴充套件帶來麻煩。

如果你在設計時想別出心裁,不遵守以上的兩套規則,那麼勸你還是不要做這樣想入非非的事。我還沒有遇到過哪一個開發者和我說設計這兩個函式要違背前面說的兩個規則,我碰到這些違反規則的情況時,都是作為設計錯誤處理。

當一個物件型別作為收集型物件的元物件時,這個物件應該擁有自己處理equals(),和/或處理hashCode()的設計,而且要遵守前面所說 的兩種原則。equals()先要查null和是否是同一型別。查同一型別是為了避免出現ClassCastException這樣的異常給丟出來。查 null是為了避免出現NullPointerException這樣的異常給丟出來。

如果你的物件裡面容納的資料過多,那麼這兩個函式 equals()和hashCode()將會變得效率低。如果物件中擁有無法serialized的資料,equals()有可能在操作中出現錯誤。想象 一個物件x,它的一個整型資料是transient型(不能被serialize成二進位制資料流)。然而equals()和hashCode()都有依靠 這個整型資料,那麼,這個物件在serialization之前和之後,是否一樣?答案是不一樣。因為serialization之前的整型資料是有效的 資料,在serialization之後,這個整型資料的值並沒有儲存下來,再重新由二進位制資料流轉換成物件後,兩者(物件在serialization 之前和之後)的狀態已經不同了。這也是要注意的。