在寫業務代碼的時候我們經常需要判斷 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 個元素