JavaRush /Java Blog /Random-TW /equals 和 hashCode 方法:使用實踐

equals 和 hashCode 方法:使用實踐

在 Random-TW 群組發布
你好!今天我們將討論Java中的兩個重要方法——equals()hashCode()。這不是我們第一次見到他們:在 JavaRush 課程開始時有一個簡短的講座,內容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:兩台機器上的所有欄位都相同,但比較的結果仍然是 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 生物測試的結果(為簡單起見 - 以代碼的形式)。您認為以下哪些特徵可以讓我們的程序識別雙胞胎親屬? 方法等於 &  hashCode:使用實踐 - 2當然,只有生物測試才能提供保證。兩個人可以有相同的眼睛顏色、髮型、鼻子,甚至疤痕——世界上有很多人,不可能避免重疊。我們需要一個可靠的機制:只有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(.........);//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,在這種情況下,額外的檢查沒有意義。考慮到所有這些,我們的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()針對自己的。
    我們已經考慮到了這項要求。我們的方法指出:

    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(),由開發人員自己重寫。就像 for 一樣equals(),該方法hashCode()具有 Oracle 文件中指定的官方要求:
  1. 如果兩個物件相等(即該方法equals()傳回 true),則它們必須具有相同的雜湊碼。

    否則我們的方法就毫無意義。hashCode()正如我們所說,應該首先檢查以提高性能。如果雜湊碼不同,即使物件實際上相等(正如我們在方法中定義的equals()),檢查也會傳回 false。

  2. 如果hashCode()對同一個物件多次呼叫一個方法,則每次都應傳回相同的數字。

  3. 相反,規則 1 則不起作用。兩個不同的物件可以具有相同的哈希碼。

第三條規則有點令人困惑。怎麼會這樣?解釋很簡單。該方法hashCode()返回int. int是一個32位數字。它的值數量有限 - 從 -2,147,483,648 到 +2,147,483,647。換句話說,這個數字的變化剛剛超過 40 億種int。現在想像一下,您正在創建一個程式來儲存地球上所有活著的人的資料。每個人都會有自己的類別物件Man。地球上生活著大約 75 億人。換句話說,無論Man我們編寫多麼好的將物件轉換為數字的演算法,我們都不會擁有足夠的數字。我們只有 45 億種選擇,而且還有更多的人。這意味著無論我們如何努力,哈希碼對於不同的人來說都是相同的。這種情況(兩個不同物件的雜湊碼匹配)稱為衝突。重寫方法時程式設計師的目標之一hashCode()是盡可能減少潛在的衝突次數。考慮到所有這些規則,我們的hashCode()類別方法會是什麼樣子?Man像這樣:
@Override
public int hashCode() {
   return dnaCode;
}
驚訝嗎?:) 出乎意料,但如果你看看這些要求,你會發現我們遵守了一切。我們傳回 true 的物件equals()將在 中相等hashCode()。如果我們的兩個物件的Man值相等equals(即它們具有相同的值dnaCode),我們的方法將傳回相同的數字。讓我們來看一個更複雜的例子。假設我們的程式應該為收藏家客戶選擇豪華汽車。收藏是一件複雜的事情,它有很多特點。一輛 1963 年生產的汽車可能比 1964 年生產的同一輛車貴 100 倍。一輛 1970 年生產的紅色汽車的價格比同年生產的相同品牌的藍色汽車貴 100 倍。 方法等於 &  hashCode:使用實踐 - 4在第一種情況下,對於 類Man,我們丟棄了大部分字段(即人員特徵),因為它們無關緊要,並且僅使用該字段進行比較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。我們現在不會詳細討論數學,但為了將來的參考,請記住將中間結果乘以足夠大的奇數有助於「分散」散列結果函數並最終獲得具有相同哈希碼的更少物件。對於 LuxuryAuto 中的方法,hashCode()它將如下所示:
@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()hashCode()我們都會選擇物件的某些字段,這些字段會在這些方法中考慮。但是我們可以考慮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;
}
以下是LuxuryAuto 類別equals()的 方法。hashCode()方法hashCode()保持不變,equals()我們從方法中刪除了該欄位model。現在該模型不是透過 來比較兩個物件的特徵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());
   }
}

Эти два an object равны друг другу?
true
Какие у них хэш-codeы?
-1372326051
1668702472
錯誤!透過使用不同的字段equals()hashCode()我們違反了為他們制定的合約!兩個相等的equals()物件必須具有相同的哈希碼。我們對它們有不同的意義。此類錯誤可能會導致最令人難以置信的後果,尤其是在處理使用雜湊的集合時。因此,重新定義時equals()hashCode()使用相同的欄位將是正確的。講座雖然很長,但今天你學到了很多新東西!:) 是時候回去解決問題了!
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION