天天看點

equals方法通用約定

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)所說:它看似簡單,但是往往很多進階程式員也無法完全正确的實作,并且如果不嚴格遵守,往往會導緻非常嚴重的後果。

equals方法通用約定

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方法,且父類實作的邏輯對于子類也是可用的。
equals方法通用約定
  • 類本身無需提供“邏輯相等”的功能。這種情況其實非常常見,比如我們在實際開發中經常寫的工具類,這些類的執行個體隻是用來完成某些任務,并不需要比較它們是否邏輯相等。比如Java提供的java.util.regex.Pattern類,并未實作equals方法,因為它覺得沒人會比較兩個Pattern對象是否相等。

2.3 重寫equals方法需要遵守哪些規則

重寫equals方法有幾條看起來很簡單,但是實作起來幾乎無法完全保證的約定:

  1. 自反性(Reflexivity):非null情況下,x.equals(x)必須為true
  2. 對稱性(Symmetry):非null情況下,x.equals(y) = true則y.equals(x) = true
  3. 傳遞性(Transitive):非null情況下,x.equals(y) = true && y.equals(z) = true則x.equals(z) = true
  4. 一緻性(Consistent):非null情況下,x.equals(y) = true隻要x或y其中任意一個對象不被修改,那麼x.equals(y) = true應該恒成立
  5. 非空性(Non-nullity):x不為null的情況下,x.equals(null)必須傳回false

看到這五條規則是不是覺得頭大,平時我們在寫的時候,壓根就沒考慮過這麼多條條框框,隻有能實作功能上的邏輯相等了就行!

equals方法通用約定

其實我覺得這麼想也不能說是完全不對,因為如果一定要完完全全的按照它這個規範來,那麼面向對象很多功能都用不了了,比如說繼承。

其實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)