天天看點

Java 重寫Object類中equals和hashCode方法

一:怎樣重寫equals()方法? 

  重寫equals()方法看起來非常簡單,但是有許多改寫的方式會導緻錯誤,并且後果非常嚴重。要想正确改寫equals()方法,你必須要遵守它的通用約定。下面是約定的内容,來自java.lang.Object的規範: 

equals方法實作了等價關系(equivalence relation): 

1. 自反性:對于任意的引用值x,x.equals(x)一定為true。 

2. 對稱性:對于任意的引用值x 和 y,當x.equals(y)傳回true時, 

  y.equals(x)也一定傳回true。 

3. 傳遞性:對于任意的引用值x、y和z,如果x.equals(y)傳回true, 

  并且y.equals(z)也傳回true,那麼x.equals(z)也一定傳回true。 

4. 一緻性:對于任意的引用值x 和 y,如果用于equals比較的對象資訊沒有被修 

  改,多次調用x.equals(y)要麼一緻地傳回true,要麼一緻地傳回false。 

5. 非空性:對于任意的非空引用值x,x.equals(null)一定傳回false。 

二:重寫equals方法的要點: 

1. 使用==操作符檢查“實參是否為指向對象的一個引用”。 

2. 使用instanceof操作符檢查“實參是否為正确的類型”。 

3. 把實參轉換到正确的類型。 

4. 對于該類中每一個“關鍵”域,檢查實參中的域與目前對象中對應的域值是否匹 

  配。對于既不是float也不是double類型的基本類型的域,可以使用==操作符 

  進行比較;對于對象引用類型的域,可以遞歸地調用所引用的對象的equals方法; 

  對于float類型的域,先使用Float.floatToIntBits轉換成int類型的值, 

  然後使用==操作符比較int類型的值;對于double類型的域,先使用 

  Double.doubleToLongBits轉換成long類型的值,然後使用==操作符比較 

  long類型的值。 

5. 當你編寫完成了equals方法之後,應該問自己三個問題:它是否是對稱的、傳 

  遞的、一緻的?(其他兩個特性通常會自行滿足)如果答案是否定的,那麼請找到 

  這些特性未能滿足的原因,再修改equals方法的代碼。

三:hashCode

hashCode主要是用于散列集合,通過對象hashCode傳回值來與散列中的對象進行比對,通過hashCode來查找散列中對象的效率為O(1),如果多個對象具有相同的hashCode,那麼散列資料結構在同一個hashCode位置處的元素為一個連結清單,需要通過周遊連結清單中的對象,并調用equals來查找元素。這也是為什麼要求如果對象通過equals比較傳回true,那麼其hashCode也必定一緻的原因。

為對象提供一個高效的hashCode算法是一個很困難的事情。理想的hashCode算法除了達到本文最開始提到的要求之外,還應該是為不同的對象産生不相同的hashCode值,這樣在操作散列的時候就完全可以達到O(1)的查找效率,而不必去周遊連結清單。假設散列中的所有元素的hashCode值都相同,那麼在散列中查找一個元素的效率就變成了O(N),這同連結清單沒有了任何的差別。

hashCode()的傳回值和equals()的關系如下:

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

四. 設計 hashCode() [1] 把某個非零常數值,例如 17 ,儲存在 int 變量 result 中; [2] 對于對象中每一個關鍵域 f (指 equals 方法中考慮的每一個域): [2.1]boolean 型,計算 (f ? 0 : 1); [2.2]byte,char,short 型,計算 (int); [2.3]long 型,計算 (int) (f ^ (f>>>32)); [2.4]float 型,計算 Float.floatToIntBits( afloat ) ; [2.5]double 型,計算 Double.doubleToLongBits( adouble ) 得到一個 long ,再執行 [2.3]; [2.6] 對象引用,遞歸調用它的 hashCode 方法 ; [2.7] 數組域,對其中每個元素調用它的 hashCode 方法。 [3] 将上面計算得到的散列碼儲存到 int 變量 c ,然後執行  result=31*result+c; [4] 傳回 result 。   這個算法存在這麼幾個問題需要探讨: 1. 為什麼初始值要使用非0的整數?這個的目的主要是為了減少hash沖突,考慮這麼個場景,如果初始值為0,并且計算hash值的前幾個域hash值計算都為0,那麼這幾個域就會被忽略掉,但是初始值不為0,這些域就不會被忽略掉,示例代碼:  

01

import

java.io.Serializable;

02

03

public

class

Test 

implements

Serializable {

04

05

private

static

final

long

serialVersionUID = 1L;

06

07

private

final

int

[] array;

08

09

public

Test(

int

... a) {

10

array = a;

11

}

12

13

@Override

14

public

int

hashCode() {

15

int

result = 

//注意,此處初始值為0

16

for

(

int

element : array) {

17

result = 

31

* result + element;

18

}

19

return

result;

20

}

21

22

public

static

void

main(String[] args) {

23

Test t = 

new

Test(

);

24

Test t2 = 

new

Test(

);

25

System.out.println(t.hashCode());

26

System.out.println(t2.hashCode());

27

}

28

29

}

如果hashCode中result的初始值為0,那麼對象t和對象t2的hashCode值都會為0,盡管這兩個對象不同。但如果result的值為17,那麼計算hashCode的時候就不會忽略這些為0的值,最後的結果t1是15699857,t2是506447   2. 為什麼每次需要使用乘法去操作result? 主要是為了使散列值依賴于域的順序,還是上面的那個例子,Test t = new Test(1, 0)跟Test t2 = new Test(0, 1), t和t2的最終hashCode傳回值是不一樣的。   3. 為什麼是31? 31是個神奇的數字,因為任何數n * 31就可以被JVM優化為 (n << 5) -n,移位和減法的操作效率要比乘法的操作效率高的多。