Java_語法基礎_equals方法與“==”的區別

例:

package deep;

public class Box {
    private int batch;

    public Box(int batch) {
        this.batch = batch;
    }

    public static void main(String[] args) {
        Box box1 = new Box(1);
        Box box2 = new Box(1);
        System.out.println(box1.equals(box2));
        System.out.println(box1 == box2);
    }
}

運行結果:
false
false

“==”運算符比較兩個Box對象(box1與box2),返回false,這個是在意料之中的,因爲box1與box2是兩個不同的對象,因而地址也不會相同。程序的原意是,如果兩個盒子的生產批次(batch)相等,就認爲是相同的盒子,否則是不同的盒子。然而,儘管box1與box2兩個對象的批次相同,但是結果卻事與願違,equals方法同樣返回了false。
請再看下面的例子:

package deep;

public class StringEquals {
    public static void main(String[] args) {
        String s1 = new String("abc");
        String s2 = new String("abc");
        StringBuilder builder1 = new StringBuilder("abc");
        StringBuilder builder2 = new StringBuilder("abc");
        StringBuffer buffer1 = new StringBuffer("abc");
        StringBuffer buffer2 = new StringBuffer("abc");
        System.out.println(s1.equals(s2));
        System.out.println(builder1.equals(builder2));
        System.out.println(buffer1.equals(buffer2));
    }
}

運行結果:
true
false
false

這又是因爲什麼呢?
出現這種現象的原因是:我們沒有弄清楚equals方法到底比較的是什麼。從其根源來看,equals方法是在Object類中聲明的,訪問修飾符爲public,而所有類(除Object類自身外)都是Object的直接或間接子類,也就是所有子類都繼承了這個方法。在Object類中,equals方法實現如下:
public boolean equals(Object obj){
return (this==obj);
}
從而可知,在Object類中,equals方法與“==”運算符是完全等價的,而我們編寫的Box類繼承了Object類中的equals方法,因此,Box類中equals方法與“==”是等價的,也就是比較的是對象的地址,而非對象的內容。對於String類,之所以該類可以比較對象的內容,那是因爲String類重寫了eruals方法,使該方法比較的是字符序列(也就是我們通常所說的內容),而非對象的地址。而對於StringBuilder與StringBuffer兩個類,與Box類相同,沒有重寫equals方法,故不同的對象,equals方法返回值爲false。
在Box類中,我們也可以重寫從Object類中繼承的equals方法,從而來實現之前期望的結果,在Box類中增加如下代碼:

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof Box) {
            Box box = (Box) obj;
            return batch == box.batch;
        }
        return false;
    }

equals的重寫規則

我們在重寫equals方法的時候,還是需要注意一些規則的。在JavaAPI文檔中,有如下幾點要求。
1.自反性。對於任何非null的引用值x,x.equals(x)應返回true。
2.對稱性。對於任何非null的引用值x與y,當且僅當:y.equals(x)返回true時,x.equals(y)才應返回true。
3.傳遞性。對於任何非null的引用值x、y與z。如果x.equals(y)返回true,並且y.equals(z)返回true,那麼x.equals(z)也應返回true。
4.一致性。對於任何非空引用值x與y,假設對象上equals比較中的信息沒有被修改,則多次調用x.equals(y)始終返回true或始終返回false。
5.對於任何非空引用值x,x.equals(null)應返回false。

蝴蝶效應

目前我們已經知道,重寫equals方法需要注意的地方。可是,是否重寫了equals方法,使其具備上面所談的幾點要求就萬事OK了呢?不是的,一旦涉及映射相關的操作(Map接口),還是會存在問題。
這是因爲實現了Map接口的類會用到鍵對象(作爲Key的對象)的哈希碼,當調用put方法加入鍵值對或調用get方法取回值對象的時候,都是根據鍵對象的哈希碼來計算存儲位置的。如果對象的哈希碼沒有相關保證,就不能夠得到預期的結果。
可以調用hashCode方法來獲得對象哈希碼,該方法在Object類中聲明,所有子類都會繼承,如下:
public native int hashCode();

在JavaAPI文檔中,關於hashCode方法有以下幾點規定。
1.在Java應用程序執行期間,如果在對象equals方法比較中所用的信息沒有被修改過,那麼在同一對象上多次調用hashCode方法時,必須一致地返回相同的整數。但如果多次執行同一個應用時,不要求該整數必須相同。
2.如果兩個對象通過調用equals方法是相等的,那麼這兩個對象調用hashCode方法必須返回相同的整數。
3.如果兩個對象通過調用equals方法是不相等的,不要求這兩個對象調用hashCode方法必須返回不同的整數。但是,程序員應該意識到對不同的對象產生不同的哈希碼值可以提高哈希表的性能。
如果一個類重寫了equals方法,但是沒有重寫hashCode方法,會發生怎樣的情況呢?那就會直接違背第2條規定,從而導致嚴重的後果。
例:

package deep;

import java.util.HashMap;
import java.util.Map;

public class AbnormalResult {
    public static void main(String[] args) {
        Map<String, Value> map1 = new HashMap<String, Value>();
        String s1 = new String("key");
        String s2 = new String("key");
        Value value = new Value(15);
        map1.put(s1, value);
        System.out.println("s1.equals(s2):" + s1.equals(s2));
        System.out.println("map.get(s1):" + map1.get(s1));
        System.out.println("map.get(s2):" + map1.get(s2));
        Map<Key, Value> map2 = new HashMap<Key, Value>();
        Key k1 = new Key("A");
        Key k2 = new Key("A");
        map2.put(k1, value);
        System.out.println("k1.equals(k2):" + k1.equals(k2));
        System.out.println("map.get(k1):" + map2.get(k1));
        System.out.println("map.get(k2):" + map2.get(k2));
    }
}

class Key {
    private String k;

    public Key(String k) {
        this.k = k;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof Key) {
            Key key = (Key) obj;
            return k.equals(key.k);
        }
        return false;
    }

}

class Value {
    private int v;

    public Value(int v) {
        this.v = v;
    }

    @Override
    public String toString() {
        return "Value [v=" + v + "]";
    }

}

運行結果:
s1.equals(s2):true
map.get(s1):Value [v=15]
map.get(s2):Value [v=15]
k1.equals(k2):true
map.get(k1):Value [v=15]
map.get(k2):null

map1加入(s1,value)鍵值對,然後使用鍵s1取出值value,這肯定是沒問題的。由於s1與s2是相等的,所以,通過鍵s2也可以取出值來。
但是,儘管k1與k2也是相等的,可是當map2加入(k1,value)鍵值對時,通過k2卻不能夠取出value值。String類與我們自定義的Key有什麼不同嗎?Key類也重寫了equals方法,並且也遵守了equals方法的規則,爲什麼還會如此呢?
根本原因就在於,HashMap類的get方法除了使用equals方法比較兩個對象之外,還使用了hashCode方法進行比較,而Key類在重寫了equals方法的同時,並沒有重寫hashCode方法,Key類使用的是從Object類繼承的hashCode方法,該方法是通過對象地址來計算哈希碼的,不同的對象哈希碼也不會相同。如此一來,通過equals比較相等的兩個對象k1和k2產生不同的哈希碼,調用put方法加入(k1,value)鍵值對時,使用k1的哈希碼計算存儲地址,而調用get方法取出對象時,使用k2的哈希碼計算存儲地址,取得的值對象自然也就爲null了。
與equals方法相似,String類在重寫了equals方法的同時,也重寫了hashCode方法。因而程序中對s1和s2的操作沒有什麼問題。所以,請記住這樣一條結論:如果一個類重寫了equals方法,那麼該類也一定要重寫hashCode方法。根據上面重寫hashCode方法的3條規定,我們在重寫該方法時,通常是equals方法中哪幾個成員變量參與了相等的比較,就使用哪幾個成員變量進行運算(運算規則根據需要自己來定),然後返回運算的結果。
回到我們上面的程序,要修正這個問題,只要在Key類中加入一個合適的hashCode方法就可以了,如下:

    @Override
    public int hashCode() {
        return k.hashCode();
    }

要點總結:

  • 從Object類繼承的equals方法與“==”運算符的比較方式是相同的。如果繼承的equals方法對我們自定義的類不適用,則可以重寫equals方法。
  • 重寫equals方法的時候,需要遵守API文檔中的5點規定,否則該類與其他類(例如實現了Collection接口或其子接口的類)交互時,很可能產生不確定的運行結果。
  • 再重寫equals方法的同時,也必須要重寫hashCode方法。否則該類與其他類(例如實現了Map接口及其子接口的類)交互時,很可能產生不確定的運行結果。
  • 重寫hashCode方法時也要遵守API文檔中的3點規定,其中第3點規定是建議性的。
發佈了133 篇原創文章 · 獲贊 11 · 訪問量 13萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章