你好!今天我們將討論 Java 中的兩個重要方法:
equals()
和hashCode()
. 這不是我們第一次見到他們:CodeGym 課程從一個簡短的課程開始equals()
——如果您忘記了或者以前沒有看過,請閱讀…… 在今天的課程中,我們將討論這些概念的詳細信息。相信我,我們有話要說!但在繼續介紹新內容之前,讓我們回顧一下我們已經介紹過的內容 :) 如您所知,使用運算符比較兩個對象通常不是一個好主意,因為比較的是==
引用==
。這是我們最近一課的汽車示例:
public class Car {
String model;
int maxSpeed;
public static void main(String[] args) {
Car car1 = new Car();
car1.model = "Ferrari";
car1.maxSpeed = 300;
Car car2 = new Car();
car2.model = "Ferrari";
car2.maxSpeed = 300;
System.out.println(car1 == car2);
}
}
控制台輸出:
false
看起來我們創建了兩個相同的Car
對象:兩個car對像對應字段的值是一樣的,但是比較的結果還是false。我們已經知道原因了:car1
和car2
引用指向不同的內存地址,所以它們不相等。但是我們仍然想比較兩個對象,而不是兩個引用。比較對象的最佳解決方案是equals()
方法。
equals() 方法
您可能還記得我們不是從頭開始創建此方法,而是覆蓋它:該equals()
方法是在Object
類中定義的。也就是說,以通常的形式,它沒什麼用處:
public boolean equals(Object obj) {
return (this == obj);
}
這就是equals()
方法在類中的定義方式Object
。這又是一次參考文獻的比較。他們為什麼要這樣做?那麼,該語言的創造者如何知道您的程序中的哪些對像被認為是相等的,哪些不是?:) 這是該方法的要點equals()
——類的創建者是確定在檢查類的對像是否相等時使用哪些特徵的人。然後重寫equals()
類中的方法。如果你不太理解“決定哪些特徵”的含義,我們來看一個例子。這是一個代表男人的簡單類:Man
.
public class Man {
private String noseSize;
private String eyesColor;
private String haircut;
private boolean scars;
private int dnaCode;
public Man(String noseSize, String eyesColor, String haircut, boolean scars, int dnaCode) {
this.noseSize = noseSize;
this.eyesColor = eyesColor;
this.haircut = haircut;
this.scars = scars;
this.dnaCode = dnaCode;
}
// Getters, setters, etc.
}
假設我們正在編寫一個程序,需要確定兩個人是同卵雙胞胎還是長相相似。我們有五個特徵:鼻子大小、眼睛顏色、髮型、疤痕的存在和 DNA 測試結果(為簡單起見,我們將其表示為整數代碼)。您認為這些特徵中的哪一個可以讓我們的程序識別同卵雙胞胎? 當然,只有DNA檢測才能提供保證。兩個人可以有相同的眼睛顏色、髮型、鼻子,甚至疤痕——世界上有很多人,無法保證沒有分身。但我們需要一個可靠的機制:只有DNA測試的結果才能讓我們做出準確的結論。equals()
這對我們的方法意味著什麼?我們需要在Man
類,考慮到我們程序的要求。該方法應該比較int dnaCode
兩個對象的字段。如果它們相等,則對象相等。
@Override
public boolean equals(Object o) {
Man man = (Man) o;
return dnaCode == man.dnaCode;
}
真的那麼簡單嗎?並不真地。我們忽略了一些事情。對於我們的對象,我們只確定了一個與建立對象相等性相關的字段:dnaCode
. 現在假設我們沒有 1 個相關字段,而是 50 個相關字段。如果兩個對象的所有 50 個字段都相等,則對象相等。這樣的場景也是有可能的。主要問題是通過比較 50 個字段來確定相等性是一個耗時且資源密集的過程。現在想像一下,除了我們的Man
類之外,我們還有一個Woman
類,其字段與Man
. 如果另一個程序員使用我們的類,他或她可以輕鬆地編寫如下代碼:
public static void main(String[] args) {
Man man = new Man(........); // A bunch of parameters in the constructor
Woman woman = new Woman(.........); // The same bunch of parameters.
System.out.println(man.equals(woman));
}
在這種情況下,檢查字段值是沒有意義的:我們可以很容易地看到我們有兩個不同類的對象,所以它們不可能相等!這意味著我們應該向該equals()
方法添加一個檢查,比較被比較對象的類。很高興我們想到了這一點!
@Override
public boolean equals(Object o) {
if (getClass() != o.getClass()) return false;
Man man = (Man) o;
return dnaCode == man.dnaCode;
}
但也許我們忘記了其他事情?嗯...至少,我們應該檢查我們不是在比較一個對象和它自己!如果引用A和B指向同一個內存地址,那麼它們就是同一個對象,我們不需要浪費時間去比較50個字段。
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (getClass() != o.getClass()) return false;
Man man = (Man) o;
return dnaCode == man.dnaCode;
}
添加檢查也沒有壞處null
:沒有對象可以等於null
。因此,如果方法參數為 null,則沒有必要進行額外檢查。考慮到所有這些,我們的equals()
類方法Man
如下所示:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Man man = (Man) o;
return dnaCode == man.dnaCode;
}
我們執行上述所有初始檢查。在一天結束時,如果:
- 我們正在比較同一類的兩個對象
- 並且比較的對像不是同一個對象
- 並且傳遞的對像不是
null
dnaCode
兩個對象的領域。覆蓋該equals()
方法時,請務必遵守以下要求:
-
自反性。
當該
equals()
方法用於將任何對象與自身進行比較時,它必須返回 true。
我們已經滿足了這個要求。我們的方法包括:if (this == o) return true;
-
對稱。
若是
a.equals(b) == true
,便b.equals(a)
要返回true
。
我們的方法也滿足這個要求。 -
傳遞性。
如果兩個對像等於某個第三個對象,則它們必須彼此相等。
如果a.equals(b) == true
和a.equals(c) == true
,則b.equals(c)
還必須返回 true。 -
堅持。
equals()
僅當涉及的字段更改時,結果才必須更改。如果兩個對象的數據不變,那麼結果equals()
一定是一樣的。 -
與 的不等式
null
。對於任何對象,
a.equals(null)
必須返回 false
這不僅僅是一組一些“有用的建議”,而是一個嚴格的約定,在 Oracle 文檔中列出
hashCode() 方法
現在讓我們談談hashCode()
方法。為什麼有必要?出於完全相同的目的——比較對象。但是我們已經有了equals()
!為什麼是另一種方法?答案很簡單:提高性能。在 Java 中使用該方法表示的哈希函數hashCode()
為任何對象返回一個固定長度的數值。在 Java 中,該hashCode()
方法為任何對象返回一個 32 位數字 ( int
)。比較兩個數字比使用該方法比較兩個對像要快得多equals()
,尤其是當該方法考慮許多字段時。如果我們的程序比較對象,使用散列碼就簡單多了。只有當對象基於hashCode()
方法相等時,比較才會進行到equals()
方法。順便說一句,這就是基於散列的數據結構的工作原理,例如,熟悉的HashMap
! 該hashCode()
方法與方法一樣equals()
,由開發人員覆蓋。和 一樣equals()
,該hashCode()
方法在 Oracle 文檔中有詳細說明的官方要求:
-
如果兩個對象相等(即
equals()
方法返回真),那麼它們必須具有相同的散列碼。否則,我們的方法將毫無意義。正如我們上面提到的,
hashCode()
檢查應該首先進行以提高性能。如果哈希碼不同,那麼檢查將返回 false,即使根據我們定義方法的方式,對象實際上是相等的equals()
。 -
如果
hashCode()
在同一對像上多次調用該方法,則每次都必須返回相同的數字。 -
規則 1 不適用於相反的方向。兩個不同的對象可以具有相同的哈希碼。
hashCode()
方法返回一個int
. Anint
是一個 32 位數字。它的值範圍有限:從 -2,147,483,648 到 +2,147,483,647。換句話說,一個 . 的可能值剛剛超過 40 億個int
。現在假設您正在創建一個程序來存儲地球上所有人的數據。每個人都會對應自己的Person
對象(類似於班級Man
)。地球上生活著約 75 億人。換句話說,無論我們為轉換編寫的算法多麼巧妙Person
對像到 int,我們根本沒有足夠的可能數字。我們只有 45 億個可能的 int 值,但有比這更多的人。這意味著無論我們多麼努力,一些不同的人都會有相同的哈希碼。當這種情況發生時(兩個不同對象的哈希碼一致),我們稱之為碰撞。覆蓋該hashCode()
方法時,程序員的目標之一是盡量減少潛在的衝突次數。考慮到所有這些規則,類hashCode()
中的方法會是什麼樣子Person
?像這樣:
@Override
public int hashCode() {
return dnaCode;
}
驚訝嗎?:) 如果您查看要求,您會發現我們完全遵守這些要求。根據,我們的equals()
方法返回 true 的對像也將相等hashCode()
。如果我們的兩個Person
對象 in 相等equals
(即它們具有相同的dnaCode
),那麼我們的方法返回相同的數字。讓我們考慮一個更困難的例子。假設我們的程序應該為汽車收藏家選擇豪華汽車。收藏可能是一種複雜的愛好,具有許多特點。一輛特定的 1963 年汽車的價格可能是 1964 年汽車的 100 倍。一輛1970年的紅車比同年同品牌的藍車貴100倍。 在我們前面的例子中,對於Person
類,我們丟棄了大部分字段(即人類特徵)作為無關緊要的並且只使用了dnaCode
比較領域。我們現在在一個非常特殊的領域工作,其中沒有無關緊要的細節!這是我們的LuxuryAuto
課程:
public class LuxuryAuto {
private String model;
private int manufactureYear;
private int dollarPrice;
public LuxuryAuto(String model, int manufactureYear, int dollarPrice) {
this.model = model;
this.manufactureYear = manufactureYear;
this.dollarPrice = dollarPrice;
}
// ...getters, setters, etc.
}
現在我們必須考慮比較中的所有領域。任何錯誤都可能使客戶損失數十萬美元,因此最好還是過於安全:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
LuxuryAuto that = (LuxuryAuto) o;
if (manufactureYear != that.manufactureYear) return false;
if (dollarPrice != that.dollarPrice) return false;
return model.equals(that.model);
}
在我們的equals()
方法中,我們沒有忘記我們之前談到的所有檢查。但是現在我們比較對象的三個字段中的每一個。對於這個程序,我們需要絕對的平等,即每個領域的平等。怎麼樣hashCode
?
@Override
public int hashCode() {
int result = model == null ? 0 : model.hashCode();
result = result + manufactureYear;
result = result + dollarPrice;
return result;
}
我們類中的字段model
是一個字符串。這很方便,因為String
類已經覆蓋了hashCode()
方法。我們計算該model
字段的哈希碼,然後將其他兩個數字字段的總和添加到它。Java 開發人員有一個減少衝突次數的簡單技巧:在計算哈希碼時,將中間結果乘以一個奇素數。最常用的數字是 29 或 31。我們現在不會深入研究數學上的微妙之處,但以後請記住,將中間結果乘以足夠大的奇數有助於“散佈”哈希函數的結果,並且,因此,減少具有相同哈希碼的對象的數量。對於我們hashCode()
在 LuxuryAuto 中的方法,它看起來像這樣:
@Override
public int hashCode() {
int result = model == null ? 0 : model.hashCode();
result = 31 * result + manufactureYear;
result = 31 * result + dollarPrice;
return result;
}
您可以在StackOverflow 上的這篇文章以及Joshua Bloch 的Effective Java一書中 閱讀更多關於此機制的所有復雜信息。最後,還有一點值得一提。每次我們覆蓋equals()
andhashCode()
方法時,我們都會選擇在這些方法中考慮的某些實例字段。這些方法考慮相同的字段。但是我們可以考慮equals()
和中的不同領域hashCode()
嗎?從技術上講,我們可以。但這是一個壞主意,原因如下:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
LuxuryAuto that = (LuxuryAuto) o;
if (manufactureYear != that.manufactureYear) return false;
return dollarPrice == that.dollarPrice;
}
@Override
public int hashCode() {
int result = model == null ? 0 : model.hashCode();
result = 31 * result + manufactureYear;
result = 31 * result + dollarPrice;
return result;
}
這是我們的equals()
和類hashCode()
的方法LuxuryAuto
。該hashCode()
方法保持不變,但我們model
從equals()
方法中刪除了該字段。模型不再是該equals()
方法比較兩個對象時使用的特徵。但是在計算哈希碼時,該字段仍會被考慮在內。結果是什麼?讓我們創造兩輛車並找出答案!
public class Main {
public static void main(String[] args) {
LuxuryAuto ferrariGTO = new LuxuryAuto("Ferrari 250 GTO", 1963, 70000000);
LuxuryAuto ferrariSpider = new LuxuryAuto("Ferrari 335 S Spider Scaglietti", 1963, 70000000);
System.out.println("Are these two objects equal to each other?");
System.out.println(ferrariGTO.equals(ferrariSpider));
System.out.println("What are their hash codes?");
System.out.println(ferrariGTO.hashCode());
System.out.println(ferrariSpider.hashCode());
}
}
Are these two objects equal to each other?
true
What are their hash codes?
-1372326051
1668702472
錯誤!equals()
通過對方法和方法使用不同的字段hashCode()
,我們違反了為它們建立的契約!根據equals()
方法相等的兩個對象必須具有相同的哈希碼。我們為他們收到了不同的價值觀。此類錯誤可能會導致絕對令人難以置信的後果,尤其是在處理使用散列的集合時。因此,當您覆蓋equals()
and時hashCode()
,您應該考慮相同的字段。這節課很長,但你今天學到了很多東西!:) 現在是時候回去解決任務了!
GO TO FULL VERSION