Java 類(lèi)的 equals 方法(學(xué)習(xí) Java 編程語(yǔ)言 048)

Object 類(lèi)中的 equals 方法用于檢測(cè)一個(gè)對(duì)象是否等于另外一個(gè)對(duì)象。Object 類(lèi)中實(shí)現(xiàn)的 equals 方法將確定兩個(gè)對(duì)象引用是否相等。這是一個(gè)合理的默認(rèn)行為:如果兩個(gè)對(duì)象引用相等,這兩個(gè)對(duì)象肯定就相等。 對(duì)于很多類(lèi)來(lái)說(shuō),這就足夠了。例如,比較兩個(gè) PrintStream 對(duì)象是否相等并沒(méi)有多大的意義。不過(guò),經(jīng)常需要基于狀態(tài)監(jiān)測(cè)對(duì)象的相等性,如果兩個(gè)對(duì)象有相同的狀態(tài),才認(rèn)為這兩個(gè)對(duì)象是相等的。

例如,如果兩個(gè)員工對(duì)象的姓名、薪水和雇用日期都一樣,就認(rèn)為它們是相等的。

class Employee
{
    private String name;
    private double salary;
    private LocalDate hireDay;

    ...

    @Override
    public boolean equals(Object otherObject)
    {
        // a quick test to see if the objects are identical
        if (this == otherObject) return true;

        // must return false if the explicit parameter is null
        if (otherObject == null) return false;

        // if the classes don't match, they can't be equal
        if (getClass() != otherObject.getClass())
            return false;
        
        // now we know otherObject is a non-null Employee
        Employee other = (Employee) otherObject;

        // test whether the fields have identical values
        return name.equals(other.name) 
                && salary == other.salary 
                && hireDay.equals(other.hireDay);
    }
}

public class Manager extends Employee
{
    ...

    @Override
    public boolean equals(Object otherObject)
    {
        if (!super.equals(otherObject)) return false;
        // super.equals checked that this and otherObject belong to the same class
        Manager other = (Manager) otherObject;
        return bonus == other.bonus;
    }
}

為了防備 name 或 hireDay 可能為 null 的情況,需要使用 Objects.equals 方法。如果兩個(gè)參數(shù)都為 null,Objects.equals(a, b) 調(diào)用將返回 true ; 如果其中一個(gè)參數(shù)為 null,則返回 false;否則,如果兩個(gè)參數(shù)都不為 null,則調(diào)用 a.equals(b)。 利用這個(gè)方法,Employee.equals 方法的最后一條語(yǔ)句要改寫(xiě)為:

return Objects.equals(name, other.name)
    && salary == other.salary
    && Objects.equals(hireDay, other.hireDay);

在子類(lèi)中定義 equals 方法時(shí),首先調(diào)用超類(lèi)的 equals。如果檢測(cè)失敗,對(duì)象就不可能相等。如果超類(lèi)中的域都相等,就需要比較子類(lèi)中的實(shí)例域。

public class Manager extends Employee
{
    ...
    public boolean equals(Object otherObject)
    {
        if (!super.equals(otherObject)) return false;
        // super.equals checked that this and otherObject belong to the same class
        Manager other = (Manager) otherObject;
        return bonus == other.bonus;
    }
}

1. 相等測(cè)試與繼承

如果隱式和顯式的參數(shù)不屬于同一個(gè)類(lèi),equals 方法將如何處理呢?這是一個(gè)很有爭(zhēng)議的問(wèn)題。在前面的例子中,如果發(fā)現(xiàn)類(lèi)不匹配,equals 方法就返冋 false。但是,許多程序員卻喜歡使用 instanceof 進(jìn)行檢測(cè):
?if (!(otherObject instanceof Employee)) return false;
這樣就允許 otherObject 屬于一個(gè)子類(lèi),但是這種方法可能招致一些麻煩。正式因?yàn)檫@些麻煩,所以建議不要采用這種處理方式。

Java 語(yǔ)言規(guī)范要求 equals 方法具有以下特性:

  1. 自反性:對(duì)于任何非空引用 x,x.equals(x) 應(yīng)該返回 true。
  2. 對(duì)稱(chēng)性:對(duì)于任何引用 x 和 y,當(dāng)且僅當(dāng) y.equals(x) 返回 true 時(shí),x.equals(y) 返回 true。
  3. 傳遞性: 對(duì)于任何引用 x、 y 和 z,如果 x.equals(y) 返回 true,y.equals(z) 返回 true,x.equals(z) 也應(yīng)該返回 true。
  4. 一致性:如果 x 和 y 引用的對(duì)象沒(méi)有發(fā)生變化,反復(fù)調(diào)用 x.equals(y) 應(yīng)該返回同樣的結(jié)果。
  5. 對(duì)于任意非空引用 x,x.equals(null)應(yīng)該返回 false。

這些規(guī)則當(dāng)然合理。你肯定不希望類(lèi)庫(kù)實(shí)現(xiàn)者在查找數(shù)據(jù)結(jié)構(gòu)中的一個(gè)元素時(shí)還要糾結(jié)調(diào)用 x.equals(y) 還是調(diào)用 y.equals(x) 的問(wèn)題。

不過(guò),就對(duì)稱(chēng)性規(guī)則來(lái)說(shuō),當(dāng)參數(shù)不屬于同一個(gè)類(lèi)的時(shí)候會(huì)有一個(gè)微妙的結(jié)果。請(qǐng)看下面的調(diào)用:
?e.equals(m);
Manager 是 Employee 的子類(lèi),e 是一個(gè) Employee 對(duì)象,m 是一個(gè) Manager 對(duì)象,并且兩個(gè)對(duì)象有相同的姓名、薪水和雇傭日期。如果在 Employee.equals 中用 instanceof 進(jìn)行檢測(cè),則返回 true,然而這意味著反過(guò)來(lái)調(diào)用:
?m.equals(e);
也需要返回 true,對(duì)稱(chēng)性不允許這個(gè)方法調(diào)用返回 false 或者拋出異常。
這就使得 Manager 類(lèi)受到了束縛。這個(gè)類(lèi)的 equals 方法必須愿意將自己與任何一個(gè) Employee 對(duì)象進(jìn)行比較,而不考慮 Manager 類(lèi)特有的那部分信息!猛然間會(huì)讓人感覺(jué) instanceof 測(cè)試并不是那么好。

有些作者認(rèn)為 getClass 檢測(cè)是有問(wèn)題的,因?yàn)樗`反了替換原則。一個(gè)經(jīng)常提到的例子,就是 AbstractSet 類(lèi)的 equals 方法,它將檢測(cè)兩個(gè)集合是否有相同元素。AbstractSet 類(lèi)有兩個(gè)具體子類(lèi):TreeSet 和 HashSet,它們分別使用不同的算法查找集合元素。但無(wú)論集合采用何種方式實(shí)現(xiàn),你肯定希望能夠比較任意的兩個(gè)集合。

不過(guò),集合是非常特殊的一個(gè)例子,應(yīng)該將 AbstractSet.equals 聲明為 final,這是因?yàn)闆](méi)有任何一個(gè)子類(lèi)需要重新頂定義集合相等性的語(yǔ)義(事實(shí)上,這個(gè)方法并沒(méi)有被聲明為 final。這樣做是為了讓子類(lèi)實(shí)現(xiàn)更高效的算法來(lái)完成相等性檢測(cè))。

就現(xiàn)在來(lái)看,有兩種完全不同的情形:

  • 如果子類(lèi)可以有自己的相等性概念,則對(duì)稱(chēng)性需求將強(qiáng)制使用 getClass 檢測(cè)。
  • 如果由超類(lèi)決定相等性概念,那么就可以使用 instanceof 檢測(cè),這樣可以在不同子類(lèi)的對(duì)象之間進(jìn)行相等性比較。

在上面的 Employee 類(lèi)和 Manager 類(lèi)例子中,只要對(duì)應(yīng)的字段相等,就認(rèn)為兩個(gè)對(duì)象相等。如果兩個(gè) Manager 對(duì)象的姓名、薪水和雇用日期均相等,而獎(jiǎng)金不相等,就認(rèn)為它們是不相同的,因此,我們要使用 getClass 檢測(cè)。
但是,假設(shè)使用員工 ID 作為相等性檢測(cè)標(biāo)準(zhǔn),并且這個(gè)相等性概念使用于所有的子類(lèi),就可以使用 instanceof 檢測(cè),而且應(yīng)該將 Employee.equals 聲明為 final。

注釋?zhuān)?/strong> 在標(biāo)準(zhǔn) Java 庫(kù)中包含 150 多個(gè) equals 方法的實(shí)現(xiàn),包括使用 instanceof 檢測(cè)、調(diào)用 getClass 檢測(cè)、捕獲 ClassCastException 或者什么也不做等各種不同做法??梢圆榭?java.sql.Timestamp 類(lèi)的 API 文檔,在這里實(shí)現(xiàn)人員不無(wú)尷尬地指出,他們讓自己陷入了困境。Timestamp 類(lèi)繼承自 java.util.Date,而后者的 equals 方法使用了一個(gè) instanceof 測(cè)試,這樣一來(lái)就無(wú)法覆蓋 equals,使之同時(shí)做到對(duì)稱(chēng)且正確。

下面給出編寫(xiě)一個(gè)完美的 equals 方法的建議:

  1. 顯式參數(shù)命名為 otherObject, 稍后需要將它轉(zhuǎn)換成另一個(gè)名為 other 的變量。
  2. 檢測(cè) this 與 otherObject 是否引用同一個(gè)對(duì)象:
    ?if (this == otherObject) return true;
    這條語(yǔ)句只是一個(gè)優(yōu)化。實(shí)際上,這是一種經(jīng)常采用的形式。因?yàn)闄z查身份要比逐個(gè)比較字段開(kāi)銷(xiāo)小。
  3. 檢測(cè) otherObject 是否為 null, 如果為 null, 返回 false。這項(xiàng)檢測(cè)是很必要的。
    ?if (otherObject == null) return false;
  4. 比較 this 與 otherObject 的類(lèi)。如果 equals 的語(yǔ)義可以在子類(lèi)中改變,就使用 getClass 檢測(cè):
    ?if (getClass() != otherObject.getClass()) return false;
    如果所有的子類(lèi)都有相同的相等性語(yǔ)義,可以使用 instanceof 檢測(cè):
    ?if (!(otherObject instanceof ClassName)) return false;
  5. 將 otherObject 強(qiáng)制轉(zhuǎn)換為相應(yīng)的類(lèi)類(lèi)型變量:
    ?ClassName other = (ClassName) otherObject
  6. 現(xiàn)在根據(jù)相等性概念的要求來(lái)比較字段。使用 == 比較基本類(lèi)型字段,使用 Objects.equals 比較對(duì)象字段。如果所有的字段都匹配,就返回 true;否則返回 false。
    return field1 == other.field1
        && Objects.equals(field2, other.field2)
        && ...;
    
    如果子類(lèi)中重新定義 equals,就要在其中包含一個(gè) super.equals(other) 調(diào)用。

提示: 對(duì)于數(shù)組類(lèi)型的字段,可以使用靜態(tài)的 Arrays.equals 方法檢測(cè)相應(yīng)的數(shù)組元素是否相等。

警告: 下面是實(shí)現(xiàn) equals 方法時(shí)的一種常見(jiàn)的錯(cuò)誤。

public class Employee
{
    public boolean equals(Employee other){
        return other != null
            && getClass() == other.getClass()
            && Objects.equals(name, other.name)
            && salary == other.salary
            && Objects.equals(hireDay, other.hireDay);
    }
    ...
}

這個(gè)方法聲明的顯式參數(shù)類(lèi)型是 Employee。因此,它沒(méi)有覆蓋 Object 類(lèi)的 equals 方法,而是定義了一個(gè)完全無(wú)關(guān)的方法。
為了避免發(fā)生這種錯(cuò)誤,可以使用 @Override 標(biāo)記要覆蓋超類(lèi)方法的那些子類(lèi)方法:
?@Override public boolean equals(Object other)
如果出現(xiàn)了錯(cuò)誤,并且正在定義一個(gè)新方法,編譯器就會(huì)報(bào)告一個(gè)錯(cuò)誤。例如,假設(shè)將下面的聲明添加到 Employee 類(lèi)中:
?@Override public boolean equals(Employee other)
就會(huì)看到一個(gè)錯(cuò)誤報(bào)告,因?yàn)檫@個(gè)方法并沒(méi)有覆蓋超類(lèi) Object 中的任何方法。

java.util.Arrays 1.2

  • static boolean equals(xxx[] a, xxx[] b) 5

    如果兩個(gè)組長(zhǎng)度相同,并且在對(duì)應(yīng)的位置上數(shù)據(jù)元素也相同,將返回 true。數(shù)組的元素類(lèi)型 xxx 可以是 Object、int、long、short、char、byte、boolean、float 或 double。

java.util.Objects 7

  • static boolean equals(Object a, Object b)

    如果 a 和 b 都為null,返回 true;如果只有其中之一為 null,則返回 false;否則返回 a.equals(b)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容