Java中hashcode和equals效能注意點 - Shai

banq發表於2022-04-17

幾周前,我 在 reddit 上遇到了這個故事, 它討論了在 Map 中使用 URL 類作為鍵的問題。這歸結為java.net.URL中 hashcode() 方法的實現非常緩慢,這使得此類在這種情況下無法使用。不幸的是,這是 Java API 規範的一部分,並且在不破壞向後相容性的情況下不再可以修復。
我們能做的是理解equals和hashcode的問題。今後如何避免此類問題?

URLs Hashcode和Equals的問題是什麼?
為了理解這個問題,我們來看看JavaDoc定義。

  • 比較這個URL與另一個物件是否相等。
  • 如果給定的物件不是一個URL,那麼這個方法立即返回錯誤。
  • 如果兩個URL物件具有相同的協議,引用相等的主機,在主機上具有相同的埠號,以及相同的檔案和檔案片段,則它們是相等的。
  • 如果兩個主機的名字都能被解析為相同的IP地址,則認為是相等的;否則,如果任何一個主機的名字都不能被解析,則主機的名字必須相等,不考慮大小寫;或者兩個主機的名字都等於空。
  • 由於主機比較需要名稱解析,因此該操作是一個阻塞性操作。
  • 因為主機比較需要名稱解析,所以這個操作是一個阻塞性操作。


這可能是不清楚的。讓我們用一個簡單的程式碼塊來澄清它:

System.out.println(new URL("http://localhost/").equals(new URL("http://127.0.0.1/")));
System.out.println(new URL("http://localhost/").hashCode() == new URL("http://127.0.0.1/").hashCode());


輸出:

true
true


對於localhost來說,這可能很簡單;
但如果我們比較帶有域名的URL,則結果就不是相等的,因為需要進行DNS查詢,我們需要做的只是呼叫hashcode()!

快速的變通方法
在這種情況下,一個快速的變通方法是避免使用URL。Sun公司在原始的JVM程式碼中深深地嵌入了這個類,但我們可以使用URI來達到大多數目的。

例如,如果我們改變上面的hashcode和equals呼叫,使用URI而不是URL,我們將得到這個結果。

System.out.println(new URI("http://localhost/").equals(new URI("http://127.0.0.1/")));
System.out.println(new URI("http://localhost/").hashCode() == new URI("http://127.0.0.1/").hashCode());


我們將得到兩個語句的錯誤。雖然這對某些用例來說可能是有問題的,但在效能上是有很大差別的。

一個更大的陷阱
如果我們所使用的map鍵都是字串,我們就不會有事。這種錯誤會在我們使用這些方法的所有地方發生。

  • Sets
  • Maps
  • Storage
  • 業務邏輯

但它更深層次。當我們用自己的Hashcode和Equals邏輯來編寫自己的類時,我們經常會被糟糕的程式碼所欺騙。

Hashcode方法的一個小的效能缺陷或一個過於簡單的版本可能會導致重大的效能缺陷,而這是很難追蹤的。
例如,由於Hashcode方法太慢或不正確,一個流操作需要更長的時間,這可能是一個長期的問題。

最佳Hashcode的實現
我們首先需要理解一些平常的實現程式碼。現在我不會展示可怕的或古老的程式碼。這是好的程式碼,但它不是最好的。

public int hashCode() {
    return Objects.hash(id, core, setting, values, sets);
}


這段程式碼一開始可能看起來沒問題,但是。。。下面才是理想的程式碼。

public int hashCode() {
    return id;
}

這段程式碼才是快速、100%獨特和正確的。除了這一點,簡直沒有理由做任何事情。
有一個例外,那就是id是一個物件。

在這種情況下,我們可能想用Objects.hashCode(id)來代替,這對null也有效。

Hashcode不是Equals
嗯,很明顯......這是你在編寫Hashcode實現時需要牢記的最重要的事情之一:
Hashcode方法必須快速執行,而且對於假的情況必須與equals一致;當然對於真的情況,這個方法可能不一定適合。

hashcode必須始終遵守這一規律:

assert(obj1.hashCode() != obj2.hashCode() && !obj1.equals(obj2));


這意味著如果Hashcode的結果是不同的,那麼這些物件一定是不同的,並且一定會從equals返回false。

但是反過來就不是這樣了。

if(obj1.hashCode() == obj2.hashCode()) {
    if(obj1.equals(obj2)) {
       // this can be false...
    }
}


因此:從效能角度看:一個Hashcode方法應該比equals法快得多。
它應該讓我們跳過潛在的昂貴的equals計算,快速索引元素。

JPA問題
JPA開發者通常只是使用一個硬編碼的hashcode 值或使用類物件hashCode().來生成雜湊碼()。

如果你讓資料庫為你生成ID,當你儲存一個物件時,它將不再等同於源物件......
一個解決方案是使用@NaturalId註解和資料型別。但這需要改變資料模型。
不幸的是,對於實體類來說,沒有像樣的解決方法。

事實上,我推測JPA開發者在使用Lombok時遇到的很多問題都是因為它為你生成了hashcode 和 equals 方法。這些可能是有問題的。
 

相關文章