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


什么是 equals 和 hashcode 方法?

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

equals 方法:Object 類中的 equals 方法用於檢測一個對象是否等於另一個對象,在 Object 類中,這個方法將判斷兩個對象是否具有相同的引用,如果兩個對象具有相同的引用,它們一定是相等的。

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

為什么要重寫 equals 和 hashcode 方法?

為什么需要重寫 equals 方法和 hashcode 方法,我想主要是基於以下兩點來考慮:

1、我們已經知道了 Object 中的 equals 方法是用來判斷兩個對象的引用是否相同,但是有時候我們並不需要判斷兩個對象的引用是否相等,我們只需要兩個對象的某個特定狀態是否相等。比如對於兩篇文章來說,我只要判斷兩篇文章的鏈接是否相同,如果鏈接相同,那么它們就是同一篇文章,我並不需要去比較其它屬性或者引用地址是否相同。

2、在某些業務場景下,我們需要使用自定義類作為哈希表的鍵,這時候我們就需要重寫,因為如果不做特定修改的話,每個對象產生的 hashcode 基本上不可能相同,而 hashcode 決定了該元素在哈希表中的位置,equals 決定了判斷邏輯,所以特殊情況下就需要重寫這兩個方法,才能符合我們的要求。

我們使用一個小 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 方法重寫了。

1、重寫 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 方法也重寫了。

2、重寫 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) {
        // 1、判斷是否等於自身
        if (this == o) return true;
        // 2、判斷 o 對象是否為空 或者類型是否為 Article 
        if (o == null || !(o instanceof  Article)) return false;
        // 3、參數類型轉換
        Article article = (Article) o;
        // 4、判斷兩個對象的 url 是否相等
        return article.url.equals(url);
    }

這一次我們使用了 @Override 標記,這樣就可以避免我們上一個重寫的錯誤,因為父類中並沒有參數為 Article 的方法,所以編譯器會報錯,這對程序員來說是非常友好的。接下來我們進行了 自反性、非空性的驗證,最后判斷兩個對象的 url 是否相等。這個 equals 方法就比上面那個要好很多,基本上沒什么大毛病了。

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

  • 1、使用 == 運算符檢查參數是否為該對象的引用。如果是,返回 true。
  • 2、使用 instanceof 運算符來檢查參數是否具有正確的類型。 如果不是,則返回 false。
  • 3、參數轉換為正確的類型。因為轉換操作在 instanceof 中已經處理過,所以它肯定會成功。
  • 4、對於類中的每個「重要」的屬性,請檢查該參數屬性是否與該對象對應的屬性相匹配。

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

@Override
 public int hashCode() { 
 return 1; 
 }

這樣寫對嗎?對錯先不管,我們先來看一下 hashcode 在 Object 中的規定:

  • 1、當在一個應用程序執行過程中,如果在 equals 方法比較中沒有修改任何信息,在一個對象上重復調用 hashCode 方法時,它必須始終返回相同的值。從一個應用程序到另一個應用程序的每一次執行返回的值可以是不一致的。
  • 2、如果兩個對象根據 equals(Object) 方法比較是相等的,那么在兩個對象上調用 hashCode 就必須產生的結果是相同的整數。
  • 3、如果兩個對象根據 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 里面的一段總結結尾吧。

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

文章不足之處,望大家多多指點,共同學習,共同進步

最后

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM