JavaRush /Java Blog /Random-TW /等於和 hashCode 合約或其他任何東西
Aleksandr Zimin
等級 1
Санкт-Петербург

等於和 hashCode 合約或其他任何東西

在 Random-TW 群組發布
當然,絕大多數 Java 程式設計師都知道方法彼此密切equals相關hashCode,並建議在其類別中一致地重寫這兩個方法。少數人知道為什麼會這樣,以及如果違反這條規則會產生什麼悲慘的後果。我建議考慮這些方法的概念,重複它們的目的並理解它們為何如此相關。和上一篇關於加載類別的文章一樣,我為自己寫了這篇文章,以便最終揭示問題的所有細節,並且不再返回第三方來源。因此,我會很高興收到建設性的批評,因為如果有什麼地方有差距,就應該消除它們。唉,這篇文章實在是太長了。

等於覆蓋規則

Java 中需要一個方法equals()來確認或否定兩個同源物件在邏輯上相等的事實。也就是說,在比較兩個物件時,程式設計師需要了解它們的有效欄位是否相等。所有欄位不必相同,因為該方法equals()意味著邏輯相等。但有時並沒有特別需要使用這種方法。正如他們所說,避免使用特定機制出現問題的最簡單方法就是不要使用它。還應該注意的是,一旦你違反了合同,equals你就無法理解其他物件和結構將如何與你的物件互動。並且隨後查找錯誤原因將非常困難。

何時不重寫此方法

  • 當類別的每個實例都是唯一的時。
  • 在更大程度上,這適用於那些提供特定行為而不是設計用於處理資料的類別。例如,類別Thread. 對他們來說equals,類別提供的方法的實作Object已經綽綽有餘了。另一個例子是枚舉類別 ( Enum)。
  • 事實上,類別不需要確定其實例的等價性。
  • 例如,對於一個類,java.util.Random根本不需要將該類別的實例相互比較,以確定它們是否可以傳回相同的隨機數序列。只是因為這個類別的性質甚至不暗示這種行為。
  • 當您要擴展的類別已經有自己的方法實現equals並且該實現的行為適合您時。
  • 例如,對於類別SetListMap實作分別equalsAbstractSetAbstractListAbstractMap中。
  • equals最後,當類別的範圍是privateorpackage-private並且您確定永遠不會呼叫此方法時,無需重寫。

等於合約

重寫方法時,equals開發人員必須遵守 Java 語言規範中定義的基本規則。
  • 反身性
  • 對於任何給定值x,表達式x.equals(x)必須傳回true
    給定- 意思是這樣的x != null
  • 對稱
  • 對於任何給定值xy,只有在返回時才x.equals(y)應返回。 truey.equals(x)true
  • 傳遞性
  • 對於任何給定值xy並且z,如果x.equals(y)返回truey.equals(z)返回truex.equals(z)則必須返回該值true
  • 一致性
  • 對於任何給定值,x重複y呼叫x.equals(y)將傳回先前呼叫此方法的值,前提是用於比較兩個物件的欄位在呼叫之間沒有更改。
  • 比較空
  • 對於任何給定值,x呼叫x.equals(null)必須返回false

等於違反合約

許多類,例如 Java Collections Framework 中的類,都依賴 method 的實現equals(),因此您不應該忽視它,因為 違反該方法的約定可能會導致應用程式運行不合理,並且在這種情況下查找原因將相當困難。根據自反性原理,每個物件都必須等價於它自己。如果違反了這個原則,當我們將一個物件新增到集合中,然後使用該方法搜尋它時,contains()我們將無法找到剛剛新增到集合中的物件。對稱條件規定任何兩個物件都必須相等,無論它們比較的順序為何。例如,如果您有一個類別僅包含一個字串類型的字段,則equals將該字段與方法中的字串進行比較將是不正確的。因為 在反向比較的情況下,該方法將始終傳回值false
// Нарушение симметричности
public class SomeStringify {
    private String s;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o instanceof SomeStringify) {
            return s.equals(((SomeStringify) o).s);
        }
        // нарушение симметричности, классы разного происхождения
        if (o instanceof String) {
            return s.equals(o);
        }
        return false;
    }
}
//Правильное определение метода equals
@Override
public boolean equals(Object o) {
    if (this == o) return true;
    return o instanceof SomeStringify &&
            ((SomeStringify) o).s.equals(s);
}
根據傳遞性 條件,如果三個物件中的任何兩個相等,那麼在這種情況下,所有三個物件都必須相等。當需要透過加入有意義的元件來擴展某個基底類別時,很容易違反此原則。例如,對於Point具有座標的類xy您需要透過展開它來新增點的顏色。為此,您需要聲明一個ColorPoint具有適當欄位的類別color。因此,如果在擴展類別中我們呼叫equals父方法,並且在父方法中我們假設只比較座標xy,那麼不同顏色但座標相同的兩個點將被認為是相等的,這是不正確的。在這種情況下,有必要教派生類區分顏色。為此,您可以使用兩種方法。但一會違反對稱性規則,二會違反傳遞性規則。
// Первый способ, нарушая симметричность
// Метод переопределен в классе ColorPoint
@Override
public boolean equals(Object o) {
    if (!(o instanceof ColorPoint)) return false;
    return super.equals(o) && ((ColorPoint) o).color == color;
}
在這種情況下,呼叫point.equals(colorPoint)將返回值true,比較colorPoint.equals(point)將返回false,因為 期望一個「它的」類別的物件。因此,對稱性規則被違反。第二種方法涉及在沒有有關點顏色的資料的情況下進行「盲」檢查,即我們有類別Point。或檢查顏色是否有相關訊息,即比較該類別的物件ColorPoint
// Метод переопределен в классе ColorPoint
@Override
public boolean equals(Object o) {
    if (!(o instanceof Point)) return false;

    // Слепая проверка
    if (!(o instanceof ColorPoint))
        return super.equals(o);

    // Полная проверка, включая цвет точки
    return super.equals(o) && ((ColorPoint) o).color == color;
}
這裡違反了傳遞性 原則,如下圖所示。假設有以下物件的定義:
ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
Point p2 = new Point(1, 2);
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);
因此,雖然滿足p1.equals(p2)和 的相等性p2.equals(p3)p1.equals(p3)但它會傳回值false。同時,第二種方法在我看來看起來不太有吸引力,因為 在某些情況下,演算法可能是盲目的,無法完全執行比較,而您可能不知道。 一點詩 一般來說,據我了解,這個問題沒有具體的解決方案。一位名叫 Kay Horstmann 的權威作者認為,您可以使用傳回物件類別的instanceof方法呼叫來替換運算子的使用getClass(),並且在開始比較物件本身之前,請確保它們屬於同一類型,並且不注意它們共同起源的事實。因此,對稱性傳遞性規則將得到滿足。但同時,在街壘的另一邊站著另一位在廣泛圈子裡同樣受人尊敬的作家約書亞·布洛赫(Joshua Bloch),他認為這種做法違反了芭芭拉·利斯科夫(Barbara Liskov)的替代原則。該原則指出“調用代碼必須在不知情的情況下以與其子類相同的方式對待基類。 ” 而在霍斯特曼提出的解決方案中,顯然違反了這項原則,因為它取決於實現。總之,事情顯然是暗的。還應該指出的是,Horstmann 闡明了應用他的方法的規則,並用簡單的英語寫道,您需要在設計類別時決定策略,如果相等性測試僅由超類別執行,您可以透過執行的操作instanceof。否則,當檢查的語義根據派生類別而變化並且方法的實作需要在層次結構中下移時,您必須使用方法getClass()。反過來,Joshua Bloch 提議放棄繼承並使用物件組合,方法是在類別中包含一個ColorPointPoint,並提供存取方法asPoint()來獲取專門關於點的資訊。這將避免違反所有規則,但是,在我看來,這將使程式碼更難以理解。第三個選項是使用 IDE 自動產生 equals 方法。順便說一句,Idea 再現了 Horstmann 世代,讓您可以選擇在超類別或其後代中實現方法的策略。最後,下一個一致性規則規定,即使物件x沒有y改變,再次呼叫它們也x.equals(y)必須傳回與先前相同的值。最終規則是任何物件都不應該等於null。這裡一切都清楚了null──這就是不確定性,物體等於不確定性嗎?目前還不清楚,即false.

確定等於的通用演算法

  1. this檢查物件參考和方法參數是否相等o
    if (this == o) return true;
  2. 檢查連結是否已定義o,即是否已定義null
    如果以後比較物件類型時,會使用到運算符instanceof,則可以跳過此項,因為false本例中會傳回此參數null instanceof Object
  3. 根據上述描述和您自己的直覺,this使用o運算子instanceof或方法來比較物件類型。getClass()
  4. 如果子類別中重寫了某個方法equals,請務必進行調用super.equals(o)
  5. 將參數類型轉換o為所需的類別。
  6. 對所有重要物件欄位進行比較:
    • 對於基本類型(除了floatdouble),使用運算符==
    • 對於參考字段,您需要呼叫他們的方法equals
    • 對於數組,可以使用循環迭代或方法Arrays.equals()
    • 對於類型floatdouble需要使用相應包裝類別的比較方法Float.compare()Double.compare()
  7. 最後,回答三個問題:所實現的方法是否對稱傳遞性同意嗎?其他兩個原則(自反性確定性)通常是自動執行的。

HashCode覆蓋規則

哈希是從物件產生的數字,描述其在某個時間點的狀態。該數字在 Java 中主要用於哈希表,例如HashMap. 在這種情況下,基於物件獲取數字的雜湊函數必須以確保元素在雜湊表中相對均勻分佈的方式實現。當函數為不同的鍵傳回相同的值時,也可以最大限度地減少衝突的可能性。

合約哈希碼

為了實作雜湊函數,語言規範定義了以下規則:
  • hashCode在同一物件上多次呼叫方法必須傳回相同的雜湊值,前提是參與計算值的物件欄位沒有更改。
  • 如果物件相等,則在兩個物件上呼叫方法hashCode應始終傳回相同的數字(equals在這些物件上呼叫方法會傳回true)。
  • 對兩個不相等的物件呼叫方法hashCode必須傳回不同的雜湊值。雖然這個要求不是強制性的,但應該考慮到它的實作會對哈希表的效能產生正面的影響。

equals 和 hashCode 方法必須一起重寫

根據上述約定,當重寫程式碼中的方法時equals,您必須始終重寫該方法hashCode。由於實際上一個類別的兩個實例是不同的,因為它們位於不同的記憶體區域,因此必須根據某些邏輯標準對它們進行比較。因此,兩個邏輯上等效的物件必須傳回相同的哈希值。 如果僅重寫其中一種方法會發生什麼情況?
  1. equals是的,hashCode不是

    假設我們equals在類別中正確定義了一個方法,並hashCode決定將該方法保留在類別中Object。那麼從方法的角度來看,equals這兩個物件在邏輯上是相等的,而從方法的角度來看,hashCode它們將沒有任何共同點。因此,透過將物件放入哈希表中,我們面臨著無法透過鍵取回它的風險。
    例如,像這樣:

    Map<Point, String> m = new HashMap<>();
    m.put(new Point(1, 1),Point A);
    // pointName == null
    String pointName = m.get(new Point(1, 1));

    顯然,正在放置的對象和正在搜尋的對像是兩個不同的對象,儘管它們在邏輯上是相等的。但是因為 它們具有不同的哈希值,因為我們違反了合同,我們可以說我們在哈希表內部的某個地方丟失了物件。

  2. hashCode是的,equals不是。

    hashCode如果我們重寫該方法並equals從類別繼承該方法的實現,會發生什麼情況Object。如您所知,equals預設方法只是將指標與物件進行比較,確定它們是否引用同一個物件。假設hashCode我們已經根據所有規範編寫了該方法,即使用 IDE 生成它,並且對於邏輯上相同的對象,它將返回相同的哈希值。顯然,透過這樣做,我們已經定義了一些用於比較兩個物件的機制。

    因此,理論上應該執行上一段的例子。但我們仍然無法在哈希表中找到我們的物件。儘管我們會接近這一點,因為至少我們會找到一個哈希表籃子,該物件將位於其中。

    要成功地在雜湊表中搜尋到對象,除了比較鍵的雜湊值外,還需要判斷鍵與搜尋到的物件的邏輯相等性。也就是說,equals如果不重寫該方法就沒有辦法。

確定hashCode的通用演算法

在這裡,在我看來,你不應該太擔心並在你最喜歡的 IDE 中產生該方法。因為所有這些位元的左右移動都是為了尋找黃金比例,即常態分佈——這是針對完全頑固的傢伙的。就我個人而言,我懷疑我能否比同一個想法做得更好、更快。

而不是下結論

因此,我們看到方法在 Java 語言中equals扮演著hashCode明確定義的角色,旨在獲得兩個物件的邏輯相等特徵。在方法的情況下,equals這與比較物件有直接關係,在hashCode間接方法的情況下,當有必要時,比如說,確定物件在雜湊表或類似資料結構中的大致位置,以便提高搜尋物件的速度。除了合約之外,equals還有hashCode一個與物件比較相關的要求。這就是compareTo介面方法Comparableequals. 此要求要求開發商必須x.equals(y) == true在 時返回x.compareTo(y) == 0。也就是說,我們看到兩個物件的邏輯比較在應用程式中的任何地方都不應該矛盾,並且應該始終一致。

來源

有效的 Java,第二版。約書亞·布洛赫. 免費翻譯一本非常好的書。 Java,專業人士的圖書館。第 1 卷。基礎知識。凱‧霍斯特曼。 少一點理論,多一點實踐。但一切都沒有像布洛赫那樣詳細分析。雖然有同樣的equals()觀點。 圖片中的資料結構。HashMap 一篇關於 Java 中的 HashMap 設備的非常有用的文章。而不是看來源。
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION