Java基礎系列-equals方法和hashCode方法

唯一浩哥發表於2019-02-18

原創文章,轉載請標註出處:《Java基礎系列-equals方法和hashCode方法》

概述

        equals方法和hashCode方法都是有Object類定義的。

public class Object {
    public native int hashCode();
    public boolean equals(Object obj) {
        return (this == obj);
    }
}

        任何的類都是Object類的子類,所有它們預設都擁有這兩個方法。
        equals方法用於定義兩個物件的比較方式,而hashCode方法是native方法,主要使用者計算物件的hash值。

equals

        equals方法主要用於定義兩個物件的比較方式,預設的比較方式是比較記憶體地址,相對於基本型別來說就是值,而相對於引用型別來說就是堆中具體物件的地址。那麼就只有值相同的基本型別,和同一個物件的兩個引用才能相等。但是在我們實際業務系統中,兩個物件的相等一般指的是兩個物件的內容相同(邏輯相同),而不是說它兩個是同一個物件,這種情況使用預設的equals就無法實現相等(因為兩個不同物件地址值一定不同),這時候我們就需要對equals方法進行重寫,定義新的比較方式。

準則

  • 自省性:對於非null的x,存在:x.equals(x)返回true
  • 對稱性:對於非null的x和y,存在:x.equals(y)==y.equals(x)
  • 傳遞性:對於非null的x、y、z,存在:當x.equals(y)返回true,y.equals(z)返回true,則x.equals(z)一定為true
  • 一致性:對於非null的x和y,多次呼叫x.equals(y)所得的結果是不變的
  • 非空性:對於非null的x,存在x.equals(null)返回false

    重寫

            其實Java中已經為我們展示瞭如何重equals方法了,最經典的就是String的equals方法:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    public boolean equals(Object anObject) {
        // 首先判斷兩個物件是不是同一個,地址相同否
        if (this == anObject) {
            return true;
        }
        // 判斷給定的物件是否是String型別,這裡instanceof關鍵字是重寫equals方法時經常使用的一個關鍵字
        // instanseof用於判斷右邊的型別是否是當前物件的型別或者超型別,超介面型別等
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            // 校驗兩個字串的長度相同否
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                // 迴圈校驗兩個字串中的每個字元是否相同
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }
}

        注意,使用instanceof在針對存在子類的情況下,可能會出現違反對稱性和傳遞性的情況,為了避免這種情況,可以通給getClass的方式比較型別。
        自定義重寫:

public class EqualsTest {
    private int id;
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    @Override
    public boolean equals(Object obj) {
        // 滿足非空性
        if(obj == null){
            return false;
        }
        // 滿足自省性
        if(this == obj){
            return true;
        }
        // 滿足對稱性、傳遞性、一致性
        if(this.getClass() == obj.getClass()
                && this.getClass().getClassLoader() == obj.getClass().getClassLoader()
                && this.id == ((EqualsTest)obj).getId()){
            return true;
        }
        return false;
    }
}

        注意:這裡如果是有不同的類載入器載入的同一類的例項也是無法相等的。

hashCode

        hashCode一般用於計算物件的hash值,它在類重寫equals的時候一起重寫,重寫它的目的是為了保證equals相同的兩個物件的hashCode結果一致,為什麼要保證這一點呢,那就歸結到java中的那幾個基於Hash實現的集合上了,比如HashMap、HashSet等,這些集合需要用到物件的hash值來參與計算定位。
        使用hashCode的目的就是為了雜湊元素,最終元素能否雜湊均勻和hashCode的實現息息相關,即為hash函式。

實現方式

  • 鏈地址法(理解):在出現hash衝突的時候,在這個位置再插入新元素,並與原有元素形成一個連結串列,類似於HashMap的實現方式
  • 開放定址法(瞭解):在出現hash衝突的時候,在當前位置的附近尋找空位來存放新元素,這種方式只需要一種資料結構,不需要引入新的資料結構。其實就是為每個hash結果準備一個探查序列,用來存放發生hash衝突的元素。
    • 線性探查法:當出現hash衝突,則在當前位置逐個向後尋找空位,將新元素儲存到找到的第一個空位,當找到最後時,需要折返到一開頭繼續查詢。由於探查序列固定,所以會引發一次叢集問題。
    • 二次探查法:出現衝突,不再逐個順序探查,而是由某種函式計算的結果序列來探查,這個函式依賴於開始下標的平方,所以叫二次探查,開始下標的不同,序列就不相同,不同序列中會有重複的下標,由於每個下標開始的探查序列是固定的,所以會引發小規模叢集,即二次叢集問題。
    • 雙重雜湊法:要解決群集,就要想辦法讓相同hash結果的序列不同,最好讓序列函式依賴於元素本身,保證當元素不同時,即使hash結果一致,但一旦發生衝突,不同的元素的序列是不同的(因為序列還要依賴元素本身,元素不同,序列結果就會不同),這樣存在兩個依賴變數的探查方法,可以極大的避免叢集問題。
  • 再HASH法(知道)
  • 建立公共溢位區法(知道)

        hashCode的實現方式並不是隨手而來的,需要考慮各種情況,選擇合適的方式來實現,舉個例子,在Java的HashMap集合中,採用的就是鏈地址法來處理hash衝突。

        參考:

相關文章