一次搞懂Java中的equals()和hashCode()
前言
如果你想要自定義物件判斷是否相等的邏輯,就改equals
吧!
如果你想要修改HashMap或是HashSet中,不重複物件的邏輯,那就修改hashCode
吧!
在物件導向的世界裡面,勢必逃不了自己建立的物件(你可能把某些功能包起來,放在某個class裡面)又或是想要建立一個Employee.class
這樣的類別,方便你建立員工的資料存入資料庫中。
而這些xxxx.class
都其實繼承Object,可以說是Object是所有Java類別的超類(Abstract Class),而我會想寫這篇的原因是因為,剛好碰到需要自定義HashSet
的需求,修改放入Set的自定義物件時,判斷物件是否相等的邏輯。
什麼意思呢?簡單來說,在程式語言中,我希望不要採用Object本身的hashCode
和equals
方法,我希望只要某些屬性的值一樣,我就視這個物件已經存在於Set裡面。而不是比較記憶體中的位置、所有屬性的value都要一樣等,我希望自定義物件相等的邏輯判斷。
如果你好奇…
- 我複寫equals的方法為什麼還要管hashCode?
- equals跟hashCode的關係是什麼?
那你可以參考這個篇文章,繼續看下去。
equals 是什麼?
可以從原始碼中看到一長串的東西,沒關係你可以跳過,讓我來跟你娓娓道來 …
equals 的特性
他裡面說了啥?面試這個應該要說得出來以下幾種特性呦!
簡單來說,equals主要遵循了以下規則來判斷物件的相等性:
- 自反性Reflexive: 我就是我
x.equals(x)
應該總是返回 true。
- 對稱性Symmetric: 我是你 你就是我 順序沒差
- 如果
x.equals(y)
返回true
,則y.equals(x)
也應返回true
。這意味著比較的順序不重要。
- 如果
- 傳遞性Transitive: 爺爺的精神跟孫子是一樣的
- 相較於Symmetric更深一層,如果
x.equals(y)
返回true
,且y.equals(z)
返回true
,則x.equals(z)
也應返回true
。 - 換句話說,如果兩個物件分別與第三個物件相等,則它們之間也應該相等。
- 相較於Symmetric更深一層,如果
- 一致性Consistent:只要內容不變還是一樣
- 只要比較時使用的信息未被修改,多次調用 x.equals(y) 應始終返回相同的結果。
- 與 null 的比較:永遠不同
- 對於任何非 null 的引用值 x,x.equals(null) 應該返回 false。物件不應該與 null 相等。
到底怎麼樣算equals?
你說的我都懂:但是我怎麼知道 equals 到底是比什麼?
按照原本equals的內容,如果滿足下面三個條件的話,我就說這兩個物件是一樣的:
- 位置 一樣
- 類別 一樣
- 屬性 一樣
上面的例子中,你可能最不懂的是比較位置…
舉例來說,下面有一個程式範例:
1 | public static void main(String[] args) { |
大概是這種感覺:當new一個物件的時候,就會給他安排在不同的位置,所以他們當然就是獨立的個體囉,就像是同名同姓的人住在不同地區一樣,但是他們還是不同人。
但是如果你把程式改成這樣:
1 | public static void main(String[] args) { |
為啥要改寫equals?
今天你不想要,判斷位置,我希望只要屬性值一樣,就識別這兩個物件是一樣的
這時候你可以考慮override equlas
方法
- 簡單來說,我有一個
IdNumber.class
,用來記錄身分證資料,因此我只需要知道id
如果一樣的話,一定是同一個人,不用多說。
1 | public class IdNumber { |
這時候你可能會不太了解為什麼不要使用instanceof
來進行類別的比較?
因為在父類別層次進行比較時,會出現問題
Ans:
違反對稱性:user 不是 employee 的子類,但是employee
是user
的子類,我們希望不管employee
還是user
放在被比較的那方,還是比較方,結果應該都要一樣!
因此才使用getClass
來進行類別的比較,不考慮父類別之間的關係,反正class不一樣,我一率覺得這是不一樣的物件,拒絕!
1 | class Parent { |
關於HashCode ?
hashCode也叫雜湊碼(雜湊碼),它用來計算物件中所有屬性的雜湊值,哪裡會使用到呢?從字面上就可以看到,Hash
有關的,就會呼叫到這個函式。例如,HashMap
或是HashSet
。在 Java 或 Kotlin 語言中,hashCode()
方法的主要目的是用來在使用哈希表(如 HashMap
或 HashSet
)時提供一種快速查找的方式。
hash 的特性
在計算機科學中,雜湊(Hash)是一種特殊的函數,具有以下幾個主要特性:
- 確定性(Deterministic):
- 對於同一個輸入,Hash 函數每次運算都將產生同一個輸出。也就是說,如果你有一個輸入 A,每次將 A 輸入 Hash 函數,你都會得到同樣的結果。
- 快速計算(Fast to Compute):
- 對任何給定的輸入,計算其 Hash 值都應該是非常迅速的。
- 不可逆性(Irreversibility):
- 當知道了 Hash 函數的輸出,我們卻無法推算出其對應的輸入。這是密碼學中特別重要的一個特性。
- 隨輸入微小變化產生大變化(Avalanche Effect):
- 即使輸入的微小變化,也應該導致 Hash 值的劇烈變化。這有助於確保相似的輸入在經過雜湊後得到的結果將顯著不同。
儲存物件於哈希表
當您在哈希表(HashMap
或是 HashSet
)中存儲物件時,hashCode()
方法被用來確定物件應該被存放在哈希表的哪個位置。這通常通過將物件的 hashCode()
返回值對哈希表的大小進行取模運算來實現。
透過hashCode檢索儲存於哈希表的物件
- 當您試圖從哈希表中檢索物件時,
hashCode()
方法也會被調用以確定應該在哪裡查找該物件。 - 如果哈希碼與表中存儲的任何物件的哈希碼不匹配,那麼哈希表可以立即確定該物件不在表中,而無需進行進一步的比較。
- 如果在該哈希碼對應的位置找到了一個或多個物件,哈希表將使用
equals()
方法來確定哪一個物件(如果有的話)與查詢的物件匹配。這是因為多個不同的物件可能會有相同的哈希碼(也就是所謂的哈希碰撞)。
這就是為什麼當我們在自定義類別中覆寫 equals()
方法時,我們應該始終確保也覆寫 hashCode()
方法,並確保如果兩個物件根據 equals()
方法是相等的,那麼它們的 hashCode()
方法也應該返回相同的值。如果這兩種方法之間的契約被違反,那麼依賴這些方法的資料結構(如 HashMap
和 HashSet
)可能無法正確地工作。
1 | import java.util.HashSet; |