在寫業務代碼的時候我們經常需要判斷 2 個 Java 物件是否一樣,常用判斷物件是否相等可以使用
==
、equals、hashcode 這 3 個方法,本文試圖搞清楚這三者的用法。
關係操作符==
#
使用 ==
判斷 2 個物件是否相等,判斷的是這 2 個物件的地址是否相等。
示例代碼:
輸出結果:
可以看出 jack 和 tom 雖然是 new 新建同一個 Person,但是新建出來的 Person 的物件地址是不一樣的,所以 jack 是不等於 tom 的,可以從打印出來的地址結果看出 jack 與 bob 的物件地址一樣,所以它倆比較的結果為 true
Equals()
方法#
Java 中所有類都有一個父類是 Object 這個類,每個類都會繼承這個類的方法,包括
equals()
方法
Equals () 的本質#
繼承父類的方法後,子類可以對父類的同名方法進行重寫,在沒有對 equals()
方法重寫的情況下,使用 equals
方法判斷 2 個物件是否相等與使用 上面所述的用 ==
操作符判斷結果是一樣的,判斷的都是這 2 個物件的地址是否相等
示例代碼:
輸出結果:
其實看一下 equals () 方法的底層源碼,其實使用的就是 ==
對 2 個物件進行判斷的
重寫 equals () 方法#
有時候我們不僅是要比較 2 個物件的地址是否一樣,還需要判斷這 2 個物件的內容是否一樣,這個時候就需要對 equals 方法進行重寫
可以對 equals
進行以下的重寫
這樣對物件的判斷就會不一樣,這次判斷的是物件的內容是否一樣
輸出結果:
可以看出,jack 物件實例與 tom 物件實例雖然物件地址不一樣,但是由於我們重寫了 equals () 方法,使得比較的重點是物件的內容,所以比較的結果是 true
其他 equals () 重寫寫法#
有許多其他封裝好的工具包都對
equals()
進行了重寫
Apache Commons Lang 框架#
equals 與 ==
的區別#
- 對於基本類型,
==
判斷兩個值是否相等,基本類型沒有 equals () 方法。 - 對於引用類型,
==
判斷兩個變量是否引用的是同一個物件,而 equals () 沒有重寫時,和==
一樣,重寫之後判斷引用的物件內容是否一樣。
hashCode()
方法#
hashCode 也是 Object 類中定義的 方法,也可以比較兩個物件是否相等,方法的返回值是調用物件的哈希值,這個哈希值的類型是
int
hashCode () 的實現#
我們看 Object 類的源碼,可以發現 hashCode () 這個方法沒有具體實現的,因為它是一個本地方法,是使用 C 語言實現的,這個方法返回的哈希值是通過將物件的內存地址轉換為整數得到的。
為什麼需要 hashCode () 方法#
上述說過的 equals 方法可以判斷物件之間是否相等,為什麼還需要 hashCode 方法呢?
在源碼的註釋中提到了這個原因:
- 支持這個方法是為了讓哈希表受益,比如
java.util.HashMap
提供的哈希表
我們知道 HashSet 和 HashMap 等集合類在往集合中添加元素的時候,都会進行一個操作:判斷當前需要加到集合的物件是否已經存放在當前集合中,這個時候就涉及到了物件之間的比較
而 HashSet 和 HashMap 都使用了 hashCode () 方法來計算物件應該存儲的位置,因此要將物件添加到這些集合類之前,都需要求出將要存儲 key 的 hashCode 值
以下是 HashMap 源碼中求 key 的 hashCode 值的方法
那為什麼要用 hashCode 方法,而不用 equals 方法呢?
原因是調用 equals 方法更加耗時
下面做了的實驗
輸出結果:
從輸出結果可以看出調用 hashCode 方法幾乎不耗時,因為本質上是比較的是 2 個 int 整型值,所以使用 hashCode 方法比較兩個物件是很快的
為什麼需要 equals () 方法#
既然 hashCode 方法這麼快,為什麼還需要 equals 方法呢?這是因為 hashCode 方法具有局限性
局限性:兩個物件的 hashCode 值相等並不代表兩個物件就相等。
這是因為 hashCode 值是通過哈希函數計算出來的,一般的計算過程是:通過對數組長度進行取模,這個的數組可以是內存數組、也可是集合數組,由於長度是有限的,就很難避免每次計算出來的哈希值都不一樣,就會產出 "哈希衝突",越糟糕的哈希算法越容易產生衝突。
哈希衝突對於比較兩個物件是否相等是有影響的,即兩個不同的物件哈希值也可能是相同的。
所以單單通過 hashCode 方法是不夠的,還需要使用 equals 方法進行進一步的判斷
HashSet 在添加元素到集合中的過程中就同時使用到這 2 個方法
- 添加元素的步驟:
- 計算物件的 hashCode 值來判斷物件加入的位置
- 與其他已經加入的物件的 hashCode 值作比較
- 如果沒有一樣的 hashCode 值,證明物件沒有在當前集合中,因為兩個相等的物件的 hashCode 值一定是相等
- 如果存在一樣的 hashCode 值,再通過 equals 方法進行進一步判斷
- 如果物件沒有在當前集合中,就把物件存放進集合
這個過程通過先用 hashCode 方法進行判斷,攔截了很多已經存在於集合中的元素調用 equals 方法的次數,提高程序執行的速度
equals 與 hashCode 的關係#
在《Effective Java》中寫到:
覆蓋 equals 時總要覆蓋 hashCode
在每個重寫了 equals 方法的類中,都必須重寫 hashCode 方法
如果只重寫了 equals,沒有重寫 hashCode,當這個類的物件作為元素加到基於散列的集合(包括 HashMap、HashSet 和 Hashtable)中就會出現問題
示例代碼:
輸出結果:
由於沒有重寫 hashCode 方法,每次創建物件的時候,都会調用 Object 類的 hashCode 方法,該方法會生成不一樣的哈希值
而前面已經提到過,HashSet 判斷一個物件是否存在當前集合,就是根據物件的哈希值判斷的,所以會導致 equals 方法與 hashCode 對比的結果不一樣,導致 HashSet 存在 2 個我們認為重複的物件
所以在重寫 equals 方法時,一定要同時重寫 hashCode 方法
那該如何重寫 hashCode 方法?
- 重寫應該遵循的原則:
- 如果物件的 equals 方法中比較時用到的信息沒有被修改的話,那麼多次調用這個物件的 hashCode 方法必須始終返回同一個值
- 如果 2 個物件 equals 方法比較時相等的,那這 2 個物件的 hashCode 方法都返回同樣的結果
輸出結果:
重寫後兩個物件的 hashCode 值一樣,set 集合元素個數也是我們預想中的結果:1 個元素