如何編寫出高質量的 equals 和 hashcode 方法?

什麼是 equals 和 hashcode 方法?

這要從 Object 類開始提及,咱們知道 Object 類是 Java 的超類,每一個類都直接或者間接的繼承了 Object 類,在 Object 中提供了 8 個基本的方法,equals 方法和 hashcode 方法就是其中的兩個。java

equals 方法:Object 類中的 equals 方法用於檢測一個對象是否等於另外一個對象,在 Object 類中,這個方法將判斷兩個對象是否具備相同的引用,若是兩個對象具備相同的引用,它們必定是相等的。程序員

hashcode 方法:用來獲取散列碼,散列碼是由對象導出的一個整數值,散列碼是沒有規律的,若是 x 和 y 是兩個不一樣的對象,那麼 x.hashCode() 與 y.hashCode() 基本上不會相同數組

爲何要重寫 equals 和 hashcode 方法?

爲何須要重寫 equals 方法和 hashcode 方法,我想主要是基於如下兩點來考慮:微信

一、咱們已經知道了 Object 中的 equals 方法是用來判斷兩個對象的引用是否相同,可是有時候咱們並不須要判斷兩個對象的引用是否相等,咱們只須要兩個對象的某個特定狀態是否相等。好比對於兩篇文章來講,我只要判斷兩篇文章的連接是否相同,若是連接相同,那麼它們就是同一篇文章,我並不須要去比較其它屬性或者引用地址是否相同。編輯器

二、在某些業務場景下,咱們須要使用自定義類做爲哈希表的鍵,這時候咱們就須要重寫,由於若是不作特定修改的話,每一個對象產生的 hashcode 基本上不可能相同,而 hashcode 決定了該元素在哈希表中的位置,equals 決定了判斷邏輯,因此特殊狀況下就須要重寫這兩個方法,才能符合咱們的要求。ide

咱們使用一個小 Demo 來模擬一下特殊場景,讓咱們更好的理解爲何須要重寫 equals 和 hashcode 方法,咱們的場景是:咱們有不少篇文章,我須要判斷文章是否已經存在 Set 中,兩篇文章相同的條件是訪問路徑相同。函數

好了,咱們一塊兒動手寫 Demo 吧,咱們創建一個文章類來存放文章信息,文章類具體設計以下:工具

class Article{
    // 文章路徑
    String url;

    // 文章標題
    String title;
    public Article(String url ,String title){
        this.url = url;
        this.title = title;
    }
    public String getUrl() {
        return url;
    }
    public void setUrl(String url) {
        this.url = url;
    }
    public String getTitle() {
        return title;
    }
    public void setTitle(String title) {
        this.title = title;
    }
}
複製代碼

文章類中有路徑、標題兩個屬性,在這個類中咱們並無重寫 equals 和 hashcode 方法,因此這裏會使用超類 Object 中的 equals 和 hashcode 方法,爲了防止你沒有看過 Object 類中的 equals 和 hashcode 方法,咱們先一塊兒來看一下 Object 的類中的 equals 和 hashcode 方法:學習

看完以後,接下來,咱們編寫一個測試類,測試類代碼以下:測試

public class EqualsAndHashcode {
    public static void main(String[] args) {
        Article article = new Article("www.baidu.com","百度一下");
        Article article1 = new Article("www.baidu.com","坑B百度");

        Set<Article> set = new HashSet<>();
        set.add(article);
        System.out.println(set.contains(article1));

    }
}
複製代碼

在測試類中,咱們實例化了兩個文章對象,文章對象的 url 都是同樣的,標題不同,咱們將 article 對象存入到 Set 中,判斷 article1 對象是否存在 Set 中,按照咱們的假設,兩篇文章的 Url 相同,則兩篇文章就應該是同一篇文章,因此這裏應該給咱們返回 True,咱們運行 Main 方法。獲得結果以下:

咱們看到告終果不是你想要的 True 而是 False ,這個緣由很簡單,由於兩篇文章的訪問路徑相同就是同一篇文章,這是咱們定義的規則,咱們並無告訴咱們的程序這個規則,咱們沒有重寫 equals 和 hashcode 方法,因此係統在判斷的時候使用的是 Object 類默認的 equals 和 hashcode 方法,默認的 equals 方法判斷的是兩個對象的引用地址是否相同,這裏確定是不同的,獲得的答案就是 False 。咱們須要把相等的規則告訴咱們的程序,那咱們就把 equals 方法重寫了。

一、重寫 equals 方法

在這裏咱們先使用 IDEA 工具生成的 equals 方法,把最後的邏輯返回邏輯修改一下就行了,具體的編寫規則咱們下面會介紹。最後咱們的 equals 方法以下

/** * 重寫equals方法,只要兩篇文章的url相同就是同一篇文章 * @param o * @return */
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Article article = (Article) o;
        return Objects.equals(url, article.url);
    }
複製代碼

再一次運行 Main 方法,你會發現仍是 False ,這是爲何呢?我已經把判斷兩個對象相等的邏輯告訴程序了,不急,咱們先來聊一聊哈希表吧,咱們知道哈希表採用的是數組+鏈表的結構,每一個數組上掛載着鏈表,鏈表的節點用來存儲對象信息,而對象落到數組的位置由 hashcode()。因此當咱們調用 HashSet 的 add(Object o) 方法時,首先會根據o.hashCode()的返回值定位到相應的數組位置,若是該數組位置上沒有結點,則將 o 放到這裏,若是已經有結點了, 則把 o 掛到鏈表末端。同理,當調用 contains(Object o) 時,Java 會經過 hashCode()的返回值定位到相應的數組位置,而後再在對應的鏈表中的結點依次調用 equals() 方法來判斷結點中的對象是不是你想要的對象。

因爲咱們只重寫了 equals 方法並無重寫 hashcode 方法,因此兩篇文章的 hashcode 值不同,這樣映射到數組的位置就不同,調用 set.contains(article1) 方法時,在哈希表中的狀況可能以下圖所示:

article 對象被映射到了數組下標爲 0 的位置,article1 對象被映射到了數組下標爲 6 的位置,因此沒有找到返回 False。既然只重寫 equals 方法不行,那麼咱們把 hashcode 方法也重寫了。

二、重寫 hashcode 方法

跟 equals 方法同樣,咱們也使用 idea 編輯器幫咱們生成的 hashcode 方法,只須要作稍微的改動就能夠,具體 hashcode 代碼以下:

@Override
    public int hashCode() {
        return Objects.hash(url);
    }
複製代碼

重寫好 hashcode 方法以後,再一次運行 Main 方法,此次獲得的結果爲 True,這會就是咱們想要的結果了。重寫 equals 和 hashcode 方法以後,在哈希表中的查找以下圖所示:

首先 article1 對象也會被映射到數組下標爲 1 的位置,在數組下標爲 1 的位置存在 article 數據節點,因此會執行 article1.equals(article) 命令,由於咱們重寫了 Article 對象的 equals 方法,這個是否會判斷兩個 Article 對象的 url 屬性是否相等,若是相等就返回 True,在這裏顯然是相等的,因此這裏就返回 True,獲得咱們想要的結果。

如何編寫 equals 和 hashcode 方法?

須要本身重寫 equals 方法?好的,我這就重寫,噼裏啪啦的敲出了下面這段代碼:

public boolean equals(Article o) {
    if (this == o) return true;
    if (o == null || !(o instanceof  Article)) return false;
    return o.url.equals(url);
}
複製代碼

這樣寫對嗎?雖然裏面的邏輯看上的沒什麼問題,可是 equals 方法的參數變成了Article。 其實你這跟重寫 equals 方法沒有半毛線關係,這徹底是從新定義了一個參數類型爲 Article 的 equals 方法,並無去覆蓋 Object 類中的 equals 方法。

那該如何重寫 equals 方法呢?其實 equals 方法是有通用規定的,當你重寫 equals 方法時,你就須要重寫 equals 方法的通用約定,在 Object 中有以下規範: equals 方法實現了一個等價關係(equivalence relation)。它有如下這些屬性:

  • 自反性:對於任何非空引用 x,x.equals(x) 必須返回 true
  • 對稱性:對於任何非空引用 x 和 y,若是且僅當 y.equals(x) 返回 true 時 x.equals(y) 必須返回 true
  • 傳遞性:對於任何非空引用 x、y、z,若是 x.equals(y) 返回 true,y.equals(z) 返回 true,則 x.equals(z) 必須返回 true
  • 一致性:對於任何非空引用 x 和 y,若是在 equals 比較中使用的信息沒有修改,則 x.equals(y) 的屢次調用必須始終返回 true 或始終返回 false
  • 非空性:對於任何非空引用 x,x.equals(null) 必須返回 false

如今咱們已經知道了寫 equals 方法的通用約定,那咱們就參照重寫 equals 方法的通用約定,再一次來重寫 Article 對象的 equals() 方法。代碼以下:

// 使用 @Override 標記,這樣就能夠避免上面的錯誤
    @Override
    public boolean equals(Object o) {
        // 一、判斷是否等於自身
        if (this == o) return true;
        // 二、判斷 o 對象是否爲空 或者類型是否爲 Article 
        if (o == null || !(o instanceof  Article)) return false;
        // 三、參數類型轉換
        Article article = (Article) o;
        // 四、判斷兩個對象的 url 是否相等
        return article.url.equals(url);
    }
複製代碼

這一次咱們使用了 @Override 標記,這樣就能夠避免咱們上一個重寫的錯誤,由於父類中並無參數爲 Article 的方法,因此編譯器會報錯,這對程序員來講是很是友好的。接下來咱們進行了 自反性、非空性的驗證,最後判斷兩個對象的 url 是否相等。這個 equals 方法就比上面那個要好不少,基本上沒什麼大毛病了。

在 effective-java 書中總結了一套編寫高質量 equals 方法的配方,配方以下:

  • 一、使用 == 運算符檢查參數是否爲該對象的引用。若是是,返回 true。
  • 二、使用 instanceof 運算符來檢查參數是否具備正確的類型。 若是不是,則返回 false。
  • 三、參數轉換爲正確的類型。由於轉換操做在 instanceof 中已經處理過,因此它確定會成功。
  • 四、對於類中的每一個「重要」的屬性,請檢查該參數屬性是否與該對象對應的屬性相匹配。

咱們已經瞭解了怎麼重寫 equals 方法了,接下來就一塊兒瞭解如何重寫 hashcode 方法,咱們知道 hashcode 方法返回的是一個 int 類型的方法,那好辦呀,像下面這樣重寫就好了

@Override
 public int hashCode() { 
 return 1; 
 }
複製代碼

這樣寫對嗎?對錯先無論,咱們先來看一下 hashcode 在 Object 中的規定:

  • 一、當在一個應用程序執行過程當中,若是在 equals 方法比較中沒有修改任何信息,在一個對象上重複調用 hashCode 方法時,它必須始終返回相同的值。從一個應用程序到另外一個應用程序的每一次執行返回的值能夠是不一致的。
  • 二、若是兩個對象根據 equals(Object) 方法比較是相等的,那麼在兩個對象上調用 hashCode 就必須產生的結果是相同的整數。
  • 三、若是兩個對象根據 equals(Object) 方法比較並不相等,則不要求在每一個對象上調用 hashCode 都必須產生不一樣的結果。

照 hashcode 規定來看,這樣寫彷佛也沒什麼問題,可是你應該知道哈希表,若是這樣寫的話,對於HashMap 和 HashSet 等散列表來講,直接把它們廢掉了,在哈列表中,元素映射到數組的哪一個位置靠 hashcode 決定,而咱們的 hashcode 始終返回 1 ,這樣的話,每一個元素都會映射到相同的位置,散列表也會退化成鏈表。

結合 hashcode 的規範和散列表來看,要重寫出一個高質量的 hashcode 方法,就須要儘量保證每一個元素產生不一樣的 hashcode 值,在 JDK 中,每一個引用類型都重寫了 hashcode 函數,咱們看看 String 類中的 hashcode 是如何重寫的:

public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        char val[] = value;

        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        hash = h;
    }
    return h;
}
複製代碼

這個 hashcode 方法寫的仍是很是好的,我我的比較喜歡用官方的東西,我以爲他們考慮的確定比咱們多不少,因此咱們 Article 類的 hashcode 方法就能夠這樣寫

/** * 重寫 hashcode方法,根據url返回hash值 * @return */
    @Override
    public int hashCode() {
        return url.hashCode();
    }
複製代碼

咱們直接調用 String 對象的 hashcode 方法。到此咱們的 equals 方法和 hashcode 方法都重寫完了,最後以 effective-java 裏面的一段總結結尾吧。

  • 一、當重寫 equals 方法時,同時也要重寫 hashCode 方法
  • 二、不要讓 equals 方法試圖太聰明。
  • 三、在 equal 時方法聲明中,不要將參數 Object 替換成其餘類型。

文章不足之處,望你們多多指點,共同窗習,共同進步

最後

打個小廣告,歡迎掃碼關注微信公衆號:「平頭哥的技術博文」,一塊兒進步吧。

平頭哥的技術博文
相關文章
相關標籤/搜索