天天看點

為什麼要重寫hashCode()方法和equals()方法以及如何進行重寫

一、前言

本篇文章主要探讨的問題有三個:

1、首先我們為什麼需要重寫hashCode()方法和equals()方法?

2、在什麼情況下需要重寫hashCode()方法和equals()方法?

3、如何重寫這兩個方法?

二、為什麼需要重寫hashCode()方法和equals()方法

首先,為什麼要重寫equals()方法。我們在定義類時,我們經常會希望兩個不同對象的某些屬性值相同時就認為他們相同,是以我們要重寫equals()方法,按照原則,我們重寫了equals()方法,也要重寫hashCode()方法。

Java中的超類Object類中定義的equals()方法是用來比較兩個引用所指向的對象的記憶體位址是否一緻,Object類中equals()方法的源碼:

public boolean equals(Object obj) {
       return (this == obj);
}
           

Object類中hashCode()方法的源碼:

public native int hashCode();
           

Object類中的hashCode()方法,用的是native關鍵字修飾,說明這個方法是個原生函數,也就說這個方法的實作不是用java語言實作的,是使用c/c++實作的,并且被編譯成了DLL,由java去調用,jdk源碼中不包含。對于不同的平台它們是不同的,java在不同的作業系統中調用不同的native方法實作對作業系統的通路,因為java語言不能直接通路作業系統底層,因為它沒有指針。

(1)這種方法調用的過程:

1、在java中申明native方法,然後編譯;

2、用javah産生一個

.h

檔案;

3、寫一個

.cpp

檔案實作native導出方法,其中需要包含第二步産生的

.h

檔案(其中又包含了jdk帶的jni.h檔案);

4、将

.cpp

檔案編譯成動态連結庫檔案;

5、在java中用

System.loadLibrary()

檔案加載第四步産生的動态連結庫檔案,然後這個navite方法就可被通路了

Java的API文檔對hashCode()方法做了詳細的說明,這也是我們重寫hashCode()方法時的原則【Object類】;

(2)重點要注意的是:

a. 在java應用程式運作時,無論何時多次調用同一個對象時的hashCode()方法,這個對象的hashCode()方法的傳回值必須是相同的一個int值;

b. 如果兩個對象equals()傳回值為true,則他們的hashCode()也必須傳回相同的int值;

c. 如果兩個對象equals()傳回值為false,則他們的hashCode()傳回值也必須不同;

現在,我們到這裡可以看出,我們重寫了equals()方法也要重寫hashCode()方法,這是因為要保證上面所述的b,c原則;是以java中的很多類都重寫了這兩個方法,例如String類,包裝類等。

三、在什麼情況下需要重寫hashCode()方法和equals()方法

當我們自定義的一個類,想要把它的執行個體儲存在集合中時,我們就需要重寫這兩個方法;集合(Collection)有兩個類,一個是List,一個是Set。

List:集合中的元素是有序的,可以重複的;
Set:無序,不可重複的;
           

(1)以HashSet來舉例:

HashSet存放元素時,根據元素的hashCode方法計算出該對象的哈希碼,快速找到要存儲的位置,然後進行比較,

比較過程如下:

  • 如果該對象哈希碼與集合已存在對象的哈希碼不一緻,則該對象沒有與其他對象重複,添加到集合中!
  • 如果存在于該對象相同的哈希碼,那麼通過equals方法判斷兩個哈希碼相同的對象是否為同一對象(判斷的标準是:屬性是否相同)
    • 相同對象,不添加。
    • 不同對象,添加。

注意:如果傳回值為false,則這個時候會以連結清單的形式在同一個位置上存放兩個元素,這會使得HashSet的性能降低,因為不能快速定位了。示意圖如下:

為什麼要重寫hashCode()方法和equals()方法以及如何進行重寫

還有一種情況就是兩個對象的hashCode()傳回值不同,但是equals()傳回true,這個時候HashSet會把這兩個對象都存進去,這就和Set集合不重複的規則相悖了;是以,我們重寫了equals()方法時,要按照b,c規則重寫hashCode()方法!

四、如何重寫這兩個方法

如果你決定要重寫equals()方法,那麼你一定還要明确這麼做所帶來的風險,并確定自己能寫出一個健壯的equals()方法。

一定要注意的一點是,在重寫equals()後,一定要重寫hashCode()方法。

(1)我們先看看 JavaSE 8 Specification中對equals()方法的說明:

The equals method implements an equivalence relation on non-null object references:

  • It is reflexive: for any non-null reference value x, x.equals(x) should return true.
  • It is symmetric: for any non-null reference values x and y, x.equals(y) should return true if and only if y.equals(x) returns true.
  • It is transitive: for any non-null reference values x, y, and z, if x.equals(y) returns true and y.equals(z) returns true, then x.equals(z) should return true.
  • It is consistent: for any non-null reference values x and y, multiple invocations of x.equals(y) consistently return true or consistently return false, provided no information used in equals comparisons on the objects is modified.
  • For any non-null reference value x, x.equals(null) should return false.

The equals method for class Object implements the most discriminating possible equivalence relation on objects; that is, for any non-null reference values x and y, this method returns true if and only if x and y refer to the same object (x == y has the value true).

這段話用了很多離散數學中的術數.簡單說明一下:

  1. 自反性:A.equals(A)要傳回true;
  2. 對稱性:如果A.equals(B)傳回true, 則B.equals(A)也要傳回true;
  3. 傳遞性:如果A.equals(B)為true, B.equals(C)為true, 則A.equals(C)也要為true. 說白了就是 A = B , B = C , 那麼A = C;
  4. 一緻性:隻要A,B對象的狀态沒有改變,A.equals(B)必須始終傳回true;
  5. A.equals(null) 要傳回false;

(2)簡單步驟:

為了說明友善,我們先定義一個程式員類(Coder):

class Coder {  
    private String name;  
    private int age;  

    // getters and setters  
} 
           

我們想要的是,如果2個程式員對象的name和age都是相同的,那麼我們就認為這兩個程式員是一個人。這時候我們就要重寫其equals()方法。因為預設的equals()實際是判斷兩個引用是否指向内在中的同一個對象,相當于 == 。 重寫時要遵循以下三步:

1、判斷是否等于自身:

if(other == this){
   return true;  
}
           

2、 使用instanceof運算符判斷 other 是否為Coder類型的對象:

if(!(other instanceof Coder))  {
   return false;  
}
           

3、比較Coder類中你自定義的資料域,name和age,一個都不能少:

Coder o = (Coder)other;  
return o.name.equals(name) && o.age == age;
           

看到這有人可能會問,第3步中有一個強制轉換,如果有人将一個Integer類的對象傳到了這個equals中,那麼會不會扔ClassCastException呢?這個擔心其實是多餘的.因為我們在第二步中已經進行了instanceof 的判斷,如果other是非Coder對象,甚至other是個null, 那麼在這一步中都會直接傳回false, 進而後面的代碼得不到執行的機會。

上面的三步也是<Effective Java>中推薦的步驟,基本可保證萬無一失。

我們在大學計算機資料結構課程中都已經學過哈希表(hash table)了,hashCode()方法就是為哈希表服務的。

當我們在使用形如HashMap, HashSet這樣前面以Hash開頭的集合類時,hashCode()就會被隐式調用以來建立哈希映射關系。

<Effective Java>中給出了一個能最大程度上避免哈希沖突的寫法,但我個人認為對于一般的應用來說沒有必要搞的這麼麻煩.如果你的應用中HashSet中需要存放上萬上百萬個對象時,那你應該嚴格遵循書中給定的方法.如果是寫一個中小型的應用,那麼下面的原則就已經足夠使用了:

要保證Coder對象中所有的成員都能在hashCode中得到展現.

@Override  
public int hashCode() {  
    int result = ;  
    result = result *  + name.hashCode();  
    result = result *  + age;  

    return result;  
}  
           

其中int result = 17你也可以改成20, 50等等都可以.看到這裡我突然有些好奇,想看一下String類中的hashCode()方法是如何實作的.查文檔知:

“Returns a hash code for this string. The hash code for a String object is computed as

s[0]*31^(n-1) + s[1]*31^(n-2) + … + s[n-1]

using int arithmetic, where s[i] is the ith character of the string, n is the length of the string, and ^ indicates exponentiation. (The hash value of the empty string is zero.)”

對每個字元的ASCII碼計算n - 1次方然後再進行加和,可見Sun對hashCode的實作是很嚴謹的. 這樣能最大程度避免2個不同的String會出現相同的hashCode的情況.

參考文章:

1、http://blog.csdn.net/jing_bufferfly/article/details/50868266

2、http://blog.csdn.net/neosmith/article/details/17068365