banner
lMingyul

lMingyul

记录穿过自己的万物
jike

這 2 個對象一樣嗎

在寫業務代碼的時候我們經常需要判斷 2 個 Java 物件是否一樣,常用判斷物件是否相等可以使用 == 、equals、hashcode 這 3 個方法,本文試圖搞清楚這三者的用法。

關係操作符==#

使用 == 判斷 2 個物件是否相等,判斷的是這 2 個物件的地址是否相等。

示例代碼:

public class Test {

    public static void main(String[] args) {
        Person jack = new Person();
        Person tom = new Person();
        Person bob = jack;

        System.out.println("jack == tom ? " + (jack == tom));
        System.out.println("jack == bob ? " + (jack == bob));
      
        System.out.println("jack 地址: " + jack);
        System.out.println("tom 地址: " + tom);
        System.out.println("bob 地址: " + bob);
    }
}

輸出結果:

jack == tom ? false
jack == bob ? true
  
jack 地址: com.mingyu.javalearn.Person@1b2c6ec2
tom 地址: com.mingyu.javalearn.Person@4edde6e5
bob 地址: com.mingyu.javalearn.Person@1b2c6ec2

可以看出 jack 和 tom 雖然是 new 新建同一個 Person,但是新建出來的 Person 的物件地址是不一樣的,所以 jack 是不等於 tom 的,可以從打印出來的地址結果看出 jack 與 bob 的物件地址一樣,所以它倆比較的結果為 true


Equals() 方法#

Java 中所有類都有一個父類是 Object 這個類,每個類都會繼承這個類的方法,包括 equals() 方法

Equals () 的本質#

繼承父類的方法後,子類可以對父類的同名方法進行重寫,在沒有對 equals() 方法重寫的情況下,使用 equals 方法判斷 2 個物件是否相等與使用 上面所述的用 == 操作符判斷結果是一樣的,判斷的都是這 2 個物件的地址是否相等

示例代碼:

public class Test {

    public static void main(String[] args) {
        Person jack = new Person();
        Person tom = new Person();
        Person bob = jack;

        System.out.println("jack == tom ? " + (jack.equals(tom)));
        System.out.println("jack == bob ? " + (jack.equals(bob)));
    }
}

輸出結果:

jack == tom ? false
jack == bob ? true

其實看一下 equals () 方法的底層源碼,其實使用的就是 == 對 2 個物件進行判斷的

public boolean equals(Object obj) {
      return (this == obj);
}

重寫 equals () 方法#

有時候我們不僅是要比較 2 個物件的地址是否一樣,還需要判斷這 2 個物件的內容是否一樣,這個時候就需要對 equals 方法進行重寫

可以對 equals 進行以下的重寫

@Override
public boolean equals(Object o) {
    // 檢查是否為同一個物件的引用,如果是直接返回 true
    if (this == o) {
      return true;
    }
    // 檢查是否是同一個類型,如果不是,直接返回 false
    if (o == null || getClass() != o.getClass()) {
      return false;
    }
    // 將 Object 物件進行轉型
    Person person = (Person) o;
    // 判斷物件內容是否相等,這裡判斷的是 name 的內容
    return Objects.equals(name, person.name);
}

這樣對物件的判斷就會不一樣,這次判斷的是物件的內容是否一樣

public class Test {

    public static void main(String[] args) {
        Person jack = new Person("jack");
        Person tom = new Person("jack");

        System.out.println("jack == tom ? " + (jack.equals(tom)));
        System.out.println("jack 地址: " + jack);
        System.out.println("tom 地址: " + tom);
    }
}

輸出結果:

jack == tom ? true
jack 地址: com.mingyu.javalearn.Person@1b2c6ec2
tom 地址: com.mingyu.javalearn.Person@4edde6e5

可以看出,jack 物件實例與 tom 物件實例雖然物件地址不一樣,但是由於我們重寫了 equals () 方法,使得比較的重點是物件的內容,所以比較的結果是 true

其他 equals () 重寫寫法#

有許多其他封裝好的工具包都對equals()進行了重寫

Apache Commons Lang 框架#

@Override
public boolean equals(Object o) {
    // 檢查是否為同一個物件的引用,如果是直接返回 true
    if (this == o) {
      return true;
    }
    // 檢查是否是同一個類型,如果不是,直接返回 false
    if (o == null || getClass() != o.getClass()) {
      return false;
    }
    // 將 Object 物件進行轉型
    Person person = (Person) o;
    // 判斷物件內容是否相等,這裡判斷的是 name 的內容
    return new EqualsBuilder().append(name, person.name).isEquals();
}

equals 與 == 的區別#

  • 對於基本類型,== 判斷兩個值是否相等,基本類型沒有 equals () 方法。
  • 對於引用類型,== 判斷兩個變量是否引用的是同一個物件,而 equals () 沒有重寫時,和== 一樣,重寫之後判斷引用的物件內容是否一樣。

hashCode()方法#

hashCode 也是 Object 類中定義的 方法,也可以比較兩個物件是否相等,方法的返回值是調用物件的哈希值,這個哈希值的類型是 int

hashCode () 的實現#

我們看 Object 類的源碼,可以發現 hashCode () 這個方法沒有具體實現的,因為它是一個本地方法,是使用 C 語言實現的,這個方法返回的哈希值是通過將物件的內存地址轉換為整數得到的。

public native int hashCode();

為什麼需要 hashCode () 方法#

上述說過的 equals 方法可以判斷物件之間是否相等,為什麼還需要 hashCode 方法呢?

在源碼的註釋中提到了這個原因:

  • 支持這個方法是為了讓哈希表受益,比如java.util.HashMap提供的哈希表

我們知道 HashSet 和 HashMap 等集合類在往集合中添加元素的時候,都会進行一個操作:判斷當前需要加到集合的物件是否已經存放在當前集合中,這個時候就涉及到了物件之間的比較

而 HashSet 和 HashMap 都使用了 hashCode () 方法來計算物件應該存儲的位置,因此要將物件添加到這些集合類之前,都需要求出將要存儲 key 的 hashCode 值

以下是 HashMap 源碼中求 key 的 hashCode 值的方法

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

那為什麼要用 hashCode 方法,而不用 equals 方法呢?

原因是調用 equals 方法更加耗時

下面做了的實驗

public class Test {

    public static void main(String[] args) {
        // 開始時間
        long stime = System.currentTimeMillis();

        // 計算執行時間
        Person jack = new Person();
        Person tom = new Person();
        System.out.println("jack == tom ? " + (jack.equals(tom)));
        
        // 結束時間
        long etime = System.currentTimeMillis();
        System.out.printf("equals()執行時長:%d 毫秒.", (etime - stime));

        System.out.println();
        System.out.println("========== 明顯的分割線 =========");
        
        // 開始時間
        stime = System.currentTimeMillis();

        // 計算執行時間
        Person mingyu = new Person();
        Person bob = new Person();
        System.out.println("mingyu == bob ? " + (mingyu.hashCode() == bob.hashCode()));

        // 結束時間
        etime = System.currentTimeMillis();
        System.out.printf("hashCode()執行時長:%d 毫秒.", (etime - stime));
    }
}

輸出結果:

jack == tom ? true
equals()執行時長:3 毫秒.
========== 明顯的分割線 =========
mingyu == bob ? false
hashCode()執行時長:0 毫秒.

從輸出結果可以看出調用 hashCode 方法幾乎不耗時,因為本質上是比較的是 2 個 int 整型值,所以使用 hashCode 方法比較兩個物件是很快的

為什麼需要 equals () 方法#

既然 hashCode 方法這麼快,為什麼還需要 equals 方法呢?這是因為 hashCode 方法具有局限性

局限性:兩個物件的 hashCode 值相等並不代表兩個物件就相等

這是因為 hashCode 值是通過哈希函數計算出來的,一般的計算過程是:通過對數組長度進行取模,這個的數組可以是內存數組、也可是集合數組,由於長度是有限的,就很難避免每次計算出來的哈希值都不一樣,就會產出 "哈希衝突",越糟糕的哈希算法越容易產生衝突。

哈希衝突對於比較兩個物件是否相等是有影響的,即兩個不同的物件哈希值也可能是相同的。

所以單單通過 hashCode 方法是不夠的,還需要使用 equals 方法進行進一步的判斷

HashSet 在添加元素到集合中的過程中就同時使用到這 2 個方法

  • 添加元素的步驟:
    • 計算物件的 hashCode 值來判斷物件加入的位置
    • 與其他已經加入的物件的 hashCode 值作比較
      • 如果沒有一樣的 hashCode 值,證明物件沒有在當前集合中,因為兩個相等的物件的 hashCode 值一定是相等
      • 如果存在一樣的 hashCode 值,再通過 equals 方法進行進一步判斷
    • 如果物件沒有在當前集合中,就把物件存放進集合

這個過程通過先用 hashCode 方法進行判斷,攔截了很多已經存在於集合中的元素調用 equals 方法的次數,提高程序執行的速度

equals 與 hashCode 的關係#

在《Effective Java》中寫到:

覆蓋 equals 時總要覆蓋 hashCode

在每個重寫了 equals 方法的類中,都必須重寫 hashCode 方法

如果只重寫了 equals,沒有重寫 hashCode,當這個類的物件作為元素加到基於散列的集合(包括 HashMap、HashSet 和 Hashtable)中就會出現問題

示例代碼:

public class Test {

    public static void main(String[] args) {

        Person person1 = new Person("jack");
        Person person2 = new Person("jack");
        HashSet<Person> set = new HashSet<>();
        set.add(person1);
        set.add(person2);

        System.out.println("person1 equals person2: " + person1.equals(person2));
        System.out.println("person1.hashCode == person2.hashCode: " + (person1.hashCode() == person2.hashCode()));
        System.out.println("person1 hashCode: " + person1.hashCode());
        System.out.println("person2 hashCode: " + person2.hashCode());

        System.out.println("set 集合元素個數: " + set.size());
        for (Person person : set) {
            System.out.println("person: " + person.getName());
        }
    }
}

輸出結果:

person1 equals person2: true
person1.hashCode == person2.hashCode: false
person1 hashCode: 455896770
person2 hashCode: 1323165413
set 集合元素個數: 2
person: jack
person: jack

由於沒有重寫 hashCode 方法,每次創建物件的時候,都会調用 Object 類的 hashCode 方法,該方法會生成不一樣的哈希值

而前面已經提到過,HashSet 判斷一個物件是否存在當前集合,就是根據物件的哈希值判斷的,所以會導致 equals 方法與 hashCode 對比的結果不一樣,導致 HashSet 存在 2 個我們認為重複的物件

所以在重寫 equals 方法時,一定要同時重寫 hashCode 方法

那該如何重寫 hashCode 方法?

  • 重寫應該遵循的原則:
    • 如果物件的 equals 方法中比較時用到的信息沒有被修改的話,那麼多次調用這個物件的 hashCode 方法必須始終返回同一個值
    • 如果 2 個物件 equals 方法比較時相等的,那這 2 個物件的 hashCode 方法都返回同樣的結果
public class Person {

    private String name;

    @Override
    public boolean equals(Object o) {
        // 檢查是否為同一個物件的引用,如果是直接返回 true
        if (this == o) {
            return true;
        }
        // 檢查是否是同一個類型,如果不是,直接返回 false
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        // 將 Object 物件進行轉型
        Person person = (Person) o;
        // 判斷物件內容是否相等,這裡判斷的是 name 的內容
        return new EqualsBuilder().append(name, person.name).isEquals();
    }

    @Override
    public int hashCode() {
      // 返回的是對 name 這個字段取哈希值
        return Objects.hash(name);
    }
}

輸出結果:

person1 equals person2: true
person1.hashCode == person2.hashCode: true
person1 hashCode: 3254270
person2 hashCode: 3254270
set 集合元素個數: 1
person: jack

重寫後兩個物件的 hashCode 值一樣,set 集合元素個數也是我們預想中的結果:1 個元素


參考資料#

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。