再也不用擔心面試官問你HashCode和equals了


結論

  1. 如果兩個對象相等,則hashcode()必須相等。

  2. 如果兩個對象相等,a.equals(b)==b.equals(a)==true

  3. 如果兩個對象有相同的hashcode值,他們也不一定是相等的。但若兩個對象相等,則hashCode值一定相等。

  4. 因此若equals()被覆蓋過,則hashCode()也必須被覆蓋。

  5. hashCOde()的默認行為是用hash算法對在heap上的對象產生獨特的值。如果沒有重載過hashCode(),則該class的兩個對象怎么都不會認為是相同的。

  6. equals()的默認行為(在Object類中的方法體)時執行==的比較,也就是說會測試兩個引用是否是heap上同一個對象,如果equals()沒有被覆蓋過,兩個對象永遠不會認為是相同的。

為什么不同的對象會有相同的hashCode()的可能?

例:hashSet使用hashCode來達成存取,這是速度比較快的方法。重點在於hashCode()相同並不一定保證對象是相等的,因為hashCode()所使用的復雜算法也許剛好會讓多個對象傳回相同的雜湊值。如果HashSet在比較的時候,發現同樣的hashCode值有多個對象,它會使用equals()來判斷這幾個對象是否真的相等。也就是說,hashCode()是用來縮小尋找范圍。但最后還是要用equals()才能確認是否真的相等。

 

HashSet中存儲元素時,先用重載過的hashCode()方法中特定算法計算出該元素的hashCode值,如果發現該集合中已經有了該hashCode值,那么調用重載過的equals()方法(比如方法體是比較該元素對象的各個屬性是否相等)來進一步判斷,如果相等那么說明該元素已經存在,否則說明不存在,該元素則存儲在該位置上的數組上

 

前言

在程序設計中,有很多的“公約”,遵守約定去實現你的代碼,會讓你避開很多坑,這些公約是前人總結出來的設計規范。

Object類是Java中的萬類之祖,其中,equals和hashCode是2個非常重要的方法。

這2個方法總是被人放在一起討論。最近在看集合框架,為了打基礎,就決定把一些細枝末節清理掉。一次性搞清楚!

下面開始剖析。

public boolean equals(Object obj)

Object類中默認的實現方式是  :   return this == obj  。那就是說,只有this 和 obj引用同一個對象,才會返回true。

而我們往往需要用equals來判斷 2個對象是否等價,而非驗證他們的唯一性。這樣我們在實現自己的類時,就要重寫equals.

按照約定,equals要滿足以下規則。

自反性:  x.equals(x) 一定是true

對null:  x.equals(null) 一定是false

對稱性:  x.equals(y)  和  y.equals(x)結果一致

傳遞性:  a 和 b equals , b 和 c  equals,那么 a 和 c也一定equals。

一致性:  在某個運行時期間,2個對象的狀態的改變不會不影響equals的決策結果,那么,在這個運行時期間,無論調用多少次equals,都返回相同的結果。

一個例子

 class Test
 2 {
 3     private int num;
 4     private String data;
 
 6     public boolean equals(Object obj)
 7     {
 8         if (this == obj)
 9             return true;
10 
11         if ((obj == null) || (obj.getClass() != this.getClass()))
12             return false;
13 
           //能執行到這里,說明obj和this同類且非null。
14         Test test = (Test) obj;
15         return num == test.num&& (data == test.data || (data != null && data.equals(test.data)));
16     }
17 
18     public int hashCode()
19     {
20         //重寫equals,也必須重寫hashCode。具體后面介紹。
24     }
25 
26 }

equals編寫指導

Test類對象有2個字段,num和data,這2個字段代表了對象的狀態,他們也用在equals方法中作為評判的依據。

在第8行,傳入的比較對象的引用和this做比較,這樣做是為了 save time ,節約執行時間,如果this 和 obj是 對同一個堆對象的引用,那么,他們一定是qeuals 的。

接着,判斷obj是不是為null,如果為null,一定不equals,因為既然當前對象this能調用equals方法,那么它一定不是null,非null 和 null當然不等價。

然后,比較2個對象的運行時類,是否為同一個類。不是同一個類,則不equals。getClass返回的是 this 和obj的運行時類的引用。如果他們屬於同一個類,則返回的是同一個運行時類的引用。注意,一個類也是一個對象。

1、有些程序員使用下面的第二種寫法替代第一種比較運行時類的寫法。應該避免這樣做。

if((obj == null) || (obj.getClass() != this.getClass())) 

     return false; 


if(!(obj instanceof Test)) 

     return false; // avoid 避免!

它違反了公約中的對稱原則。

例如:假設Dog擴展了Aminal類。

dog instanceof Animal      得到true

animal instanceof Dog      得到false

這就會導致

animal.equls(dog) 返回true
dog.equals(animal) 返回false

僅當Test類沒有子類的時候,這樣做才能保證是正確的。

2、按照第一種方法實現,那么equals只能比較同一個類的對象,不同類對象永遠是false。但這並不是強制要求的。一般我們也很少需要在不同的類之間使用equals。

3、在具體比較對象的字段的時候,對於基本值類型的字段,直接用 == 來比較(注意浮點數的比較,這是一個坑)對於引用類型的字段,你可以調用他們的equals,當然,你也需要處理字段為null 的情況。對於浮點數的比較,我在看Arrays.binarySearch的源代碼時,發現了如下對於浮點數的比較的技巧:

if ( Double.doubleToLongBits(d1) == Double.doubleToLongBits(d2) ) //d1 和 d2 是double類型

if(  Float.floatToIntBits(f1) == Float.floatToIntBits(f2)  )      //f1 和 f2 是d2是float類型

4、並不總是要將對象的所有字段來作為equals 的評判依據,那取決於你的業務要求。比如你要做一個家電功率統計系統,如果2個家電的功率一樣,那就有足夠的依據認為這2個家電對象等價了,至少在你這個業務邏輯背景下是等價的,並不關心他們的價錢啊,品牌啊,大小等其他參數。

5、最后需要注意的是,equals 方法的參數類型是Object,不要寫錯!

public int hashCode()

這個方法返回對象的散列碼,返回值是int類型的散列碼。
對象的散列碼是為了更好的支持基於哈希機制的Java集合類,例如 Hashtable, HashMap, HashSet 等。

關於hashCode方法,一致的約定是:
重寫了euqls方法的對象必須同時重寫hashCode()方法。

如果2個對象通過equals調用后返回是true,那么這個2個對象的hashCode方法也必須返回同樣的int型散列碼

如果2個對象通過equals返回false,他們的hashCode返回的值允許相同。(然而,程序員必須意識到,hashCode返回獨一無二的散列碼,會讓存儲這個對象的hashtables更好地工作。)

在上面的例子中,Test類對象有2個字段,num和data,這2個字段代表了對象的狀態,他們也用在equals方法中作為評判的依據。那么, 在hashCode方法中,這2個字段也要參與hash值的運算,作為hash運算的中間參數。這點很關鍵,這是為了遵守:2個對象equals,那么 hashCode一定相同規則。

也是說,參與equals函數的字段,也必須都參與hashCode 的計算。

合乎情理的是:同一個類中的不同對象返回不同的散列碼。典型的方式就是根據對象的地址來轉換為此對象的散列碼,但是這種方式對於Java來說並不是唯一的要求的
的實現方式。通常也不是最好的實現方式。

相比 於 equals公認實現約定,hashCode的公約要求是很容易理解的。有2個重點是hashCode方法必須遵守的。約定的第3點,其實就是第2點的
細化,下面我們就來看看對hashCode方法的一致約定要求。

第一:在某個運行時期間,只要對象的(字段的)變化不會影響equals方法的決策結果,那么,在這個期間,無論調用多少次hashCode,都必須返回同一個散列碼。
第二:通過equals調用返回true 的2個對象的hashCode一定一樣。
第三:通過equasl返回false 的2個對象的散列碼不需要不同,也就是他們的hashCode方法的返回值允許出現相同的情況。
總結一句話:等價的(調用equals返回true)對象必須產生相同的散列碼。不等價的對象,不要求產生的散列碼不相同。

hashCode編寫指導

在編寫hashCode時,你需要考慮的是,最終的hash是個int值,而不能溢出。不同的對象的hash碼應該盡量不同,避免hash沖突。

那么如果做到呢?下面是解決方案。

1、定義一個int類型的變量 hash,初始化為 7。

接下來讓你認為重要的字段(equals中衡量相等的字段)參入散列運,算每一個重要字段都會產生一個hash分量,為最終的hash值做出貢獻(影響)

運算方法參考表
重要字段var的類型 他生成的hash分量
byte, char, short , int (int)var
long  (int)(var ^ (var >>> 32))
boolean var?1:0
float  Float.floatToIntBits(var)
 double  long bits = Double.doubleToLongBits(var);
分量 = (int)(bits ^ (bits >>> 32));
 引用類型   (null == var ? 0 : var.hashCode())

 

最后把所有的分量都總和起來,注意並不是簡單的相加。選擇一個倍乘的數字31,參與計算。然后不斷地遞歸計算,直到所有的字段都參與了。

int hash = 7;

hash = 31 * hash + 字段1貢獻分量;

hash = 31 * hash + 字段2貢獻分量;

.....

return hash;

說明,以下的內容是我在google上找到並翻譯整理的,其中加入了自己的話和一些例子,便於理解,但我能保證這並不影響整體准確性。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM