CodeGym /Java Blog /Toto sisi /equals 和 hashCode 方法:最佳實踐
John Squirrels
等級 41
San Francisco

equals 和 hashCode 方法:最佳實踐

在 Toto sisi 群組發布
你好!今天我們將討論 Java 中的兩個重要方法:equals()hashCode(). 這不是我們第一次見到他們:CodeGym 課程從一個簡短的課程開始equals()——如果您忘記了或者以前沒有看過,請閱讀…… equals 和 hashCode 方法:最佳實踐 - 1在今天的課程中,我們將討論這些概念的詳細信息。相信我,我們有話要說!但在繼續介紹新內容之前,讓我們回顧一下我們已經介紹過的內容 :) 如您所知,使用運算符比較兩個對象通常不是一個好主意,因為比較的是==引用==。這是我們最近一課的汽車示例:

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。我們已經知道原因了:car1car2引用指向不同的內存地址,所以它們不相等。但是我們仍然想比較兩個對象,而不是兩個引用。比較對象的最佳解決方案是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 測試結果(為簡單起見,我們將其表示為整數代碼)。您認為這些特徵中的哪一個可以讓我們的程序識別同卵雙胞胎? equals 和 hashCode 方法:最佳實踐 - 2當然,只有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()方法時,請務必遵守以下要求:
  1. 自反性。

    當該equals()方法用於將任何對象與自身進行比較時,它必須返回 true。
    我們已經滿足了這個要求。我們的方法包括:

    
    if (this == o) return true;
    

  2. 對稱。

    若是a.equals(b) == true,便b.equals(a)要返回true
    我們的方法也滿足這個要求。

  3. 傳遞性。

    如果兩個對像等於某個第三個對象,則它們必須彼此相等。
    如果a.equals(b) == truea.equals(c) == true,則b.equals(c)還必須返回 true。

  4. 堅持。

    equals()僅當涉及的字段更改時,結果才必須更改。如果兩個對象的數據不變,那麼結果equals()一定是一樣的。

  5. 與 的不等式null

    對於任何對象,a.equals(null)必須返回 false
    這不僅僅是一組一些“有用的建議”,而是一個嚴格的約定,在 Oracle 文檔中列出

hashCode() 方法

現在讓我們談談hashCode()方法。為什麼有必要?出於完全相同的目的——比較對象。但是我們已經有了equals()!為什麼是另一種方法?答案很簡單:提高性能。在 Java 中使用該方法表示的哈希函數hashCode()為任何對象返回一個固定長度的數值。在 Java 中,該hashCode()方法為任何對象返回一個 32 位數字 ( int)。比較兩個數字比使用該方法比較兩個對像要快得多equals(),尤其是當該方法考慮許多字段時。如果我們的程序比較對象,使用散列碼就簡單多了。只有當對象基於hashCode()方法相等時,比較才會進行到equals()方法。順便說一句,這就是基於散列的數據結構的工作原理,例如,熟悉的HashMap! 該hashCode()方法與方法一樣equals(),由開發人員覆蓋。和 一樣equals(),該hashCode()方法在 Oracle 文檔中有詳細說明的官方要求:
  1. 如果兩個對象相等(即equals()方法返回真),那麼它們必須具有相同的散列碼。

    否則,我們的方法將毫無意義。正如我們上面提到的,hashCode()檢查應該首先進行以提高性能。如果哈希碼不同,那麼檢查將返回 false,即使根據我們定義方法的方式,對象實際上是相等的equals()

  2. 如果hashCode()在同一對像上多次調用該方法,則每次都必須返回相同的數字。

  3. 規則 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倍。 equals 和 hashCode 方法:最佳實踐 - 4在我們前面的例子中,對於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()方法保持不變,但我們modelequals()方法中刪除了該字段。模型不再是該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(),您應該考慮相同的字段。這節課很長,但你今天學到了很多東西!:) 現在是時候回去解決任務了!
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION