1、簡介
Java程式員都知道java.lang.Object類,這是所有類的超類。Object類中提供了幾個public的方法,比如:
public boolean equals(Object var1) {
return this == var1;
}
public String toString() {
return this.getClass().getName() + "@" + Integer.toHexString(this.hashCode());
這些public方法第一是提供給所有的子類去擴充(覆寫),第二是明确了Java中的類所具備的通用約定。是以Java中的類在覆寫這些方法是,都需要遵守通用約定,避免程式員們各玩各的。
那具體應該怎麼覆寫,又應該遵守那些通用約定呢?
其實這是一個非常複雜的問題,正如Java大師約書亞·布洛克(Joshua Bloch)所說:它看似簡單,但是往往很多進階程式員也無法完全正确的實作,并且如果不嚴格遵守,往往會導緻非常嚴重的後果。

2、正文
2.1 什麼時候需要重寫equals方法
總結一句話就是:當我們需要比較兩個對象是否“邏輯相等”時,可能需要考慮重寫equals方法,比如我們需要比較值類型的類Integer、String,這些類經常需要用于承載和比較值是否相等,或者用于做個Map、Set等集合的Key值,在這些場景下我們是需要嚴格的去重寫equals方法的。(我這裡說的是可能,是因為很多情況下無招勝有招,我們或許不需要重寫equals方法,至于那些場景不需要重寫equals方法這個會在後面說!)
2.2 什麼時候不需要重寫equals方法
不需要實作equals方法的場景非常多,我們大緻的舉例說明一下:
- 類的每個執行個體唯一。比如說:枚舉類型,枚舉類型雖然也屬于上面說的“值類”,但是由于枚舉類的每個值隻會存在一個對象,是以不需要重寫equals方法
- 類的通路權限是私有的(類私有、包級私有),并且確定equals方法不會被調用。說白了就是其他類無法調用到這個類的equals方法
- 超類覆寫的equals方法,在子類仍然适用。這種情況下我們就無需再多此一舉了,在Java的JDK源碼中,set、List、Map都直接使用了超類的equals方法,比如在HashSet提供的方法中,并未有equals方法的實作,這是因為其父類AbstractSet中覆寫了equals方法,且父類實作的邏輯對于子類也是可用的。
- 類本身無需提供“邏輯相等”的功能。這種情況其實非常常見,比如我們在實際開發中經常寫的工具類,這些類的執行個體隻是用來完成某些任務,并不需要比較它們是否邏輯相等。比如Java提供的java.util.regex.Pattern類,并未實作equals方法,因為它覺得沒人會比較兩個Pattern對象是否相等。
2.3 重寫equals方法需要遵守哪些規則
重寫equals方法有幾條看起來很簡單,但是實作起來幾乎無法完全保證的約定:
- 自反性(Reflexivity):非null情況下,x.equals(x)必須為true
- 對稱性(Symmetry):非null情況下,x.equals(y) = true則y.equals(x) = true
- 傳遞性(Transitive):非null情況下,x.equals(y) = true && y.equals(z) = true則x.equals(z) = true
- 一緻性(Consistent):非null情況下,x.equals(y) = true隻要x或y其中任意一個對象不被修改,那麼x.equals(y) = true應該恒成立
- 非空性(Non-nullity):x不為null的情況下,x.equals(null)必須傳回false
看到這五條規則是不是覺得頭大,平時我們在寫的時候,壓根就沒考慮過這麼多條條框框,隻有能實作功能上的邏輯相等了就行!
其實我覺得這麼想也不能說是完全不對,因為如果一定要完完全全的按照它這個規範來,那麼面向對象很多功能都用不了了,比如說繼承。
其實Java的JDK中也是有些代碼不滿足上面說的這五條規範的,比如我們看下如下這段代碼(猜猜它會輸出什麼?):
package com.lizba.tips;
import java.sql.Timestamp;
import java.util.Date;
/**
* <p>
* Java自帶jdk equals方法的對稱性測試
* </p>
*
* @Author: Liziba
* @Date: 2021/10/24 14:48
*/
public class EqualsDemo {
public static void main(String[] args) {
Date date = new Date();
Timestamp timestamp = new Timestamp(date.getTime());
System.out.println("Date equals to Timestamp: " + date.equals(timestamp));
System.out.println("Timestamp equals to Date: " + timestamp.equals(date));
}
是的你沒看錯,第一個輸出了true,第二個輸出了false。很顯然這不滿足第二點:對稱性(Symmetry)。
Date equals to Timestamp: true
Timestamp equals to Date: false
基于這種情況,Java并沒有很好的辦法去解決。隻能說告訴你不用混用Date和Timestamp,并且無論如何不要去equals比較Date和Timestamp,這個在Timestamp中的類和equals方法上也是有說明的!
2.4 實作高品質equals方法的訣竅
上面聊了一些什麼時候需要重寫equals方法、什麼時候不需要重寫equals方法、重寫equals方法需要遵守的規則。這裡我們聊一聊實作高品質equals方法的訣竅。
在這裡我将會引用java.util.AbstractSet類中的equals方法來闡述如何寫一個高品質的equals方法,因為小捌發現它非常經典。
java.util.AbstractSet中的equals方法:
public abstract class AbstractSet<E> extends AbstractCollection<E> implements Set<E> {
// ...
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof Set))
return false;
Collection<?> c = (Collection<?>) o;
if (c.size() != size())
try {
return containsAll(c);
} catch (ClassCastException unused) {
} catch (NullPointerException unused) {
}
第一點:o == this
使用==操作符,判斷比較對象和目前對象的引用是否相等,如果相等代表同一個對象,那就直接傳回true。
第二點:o instanceof Set
通過instanceof操作符檢查參數類型是否正确,如果類型都不對就不需要比較了。
第三點:c.size() != size()
這是在Abstract中的特殊存在,并不是所有的都需要這樣比較,提前比較大小的好處是無需進行每個域的比較,如果大小都不相等,就可以直接傳回了。通常情況下,這樣做性能更好!
第四點:containsAll(c)
對該類中的每一個域進行比較,如果所有的域都相等則傳回true,如果不相等傳回false。
第五點:重寫hashcode
重寫equals方法時一定要重寫hashcode方法,比如java.util.AbstractSet中重寫了hashCode()方法,它将每個域的hashcode進行了拼接
public int hashCode() {
int h = 0;
Iterator<E> i = iterator();
while (i.hasNext()) {
E obj = i.next();
if (obj != null)
h += obj.hashCode();
return h;
第六點:不要修改equals(Object o)的參數類型
這一點看似很簡單,但是如果你不是用IDE自動生成的equals方法,而是自己手動敲得代碼,很容易會将Object類型,改成目前類的類型,這種做法是不對的哈!因為這不是重寫(Override),這是重載(Overload)