你为什么要关心equals和hashcode
等于和哈希碼是每個Java對象的基本元素。 它們的正確性和性能對于您的應用程序至關重要。 但是,我們經常看到甚至有經驗的程序員都忽略了類開發的這一部分。 在本文中,我將介紹一些與這兩種非常基本的方法有關的常見錯誤和問題。
合同
提到的方法至關重要的是所謂的“合同”。 有大約的hashCode三個規則和五個約等于 (你可以找到他們在Java文檔的Object類),但我們將討論三個重要的。 讓我們從hashCode()開始:
“只要在Java應用程序執行期間在同一個對象上多次調用它, hashCode方法就必須一致地返回相同的整數,只要沒有 修改該對象的equals比較中使用的 信息即可 。”
這意味著對象的哈希碼不必是不變的。 因此,讓我們看一下真正簡單的Java對象的代碼:
您可能已經注意到, equals和hashCode是由我們的IDE自動生成的。 我們確信這些方法不是一成不變的,并且肯定會廣泛使用此類。 也許這樣的類太常見了,這樣的實現沒有錯嗎? 因此,讓我們看一個簡單的用法示例:
def "should find cart for given customer after correcting email address"() {given:Cart sampleCart = new Cart()Customer sampleCustomer = new Customer()sampleCustomer.setId(UUID.randomUUID())sampleCustomer.setEmail("emaill@customer.com")HashMap customerToCart = new HashMap<>()when:customerToCart.put(sampleCustomer, sampleCart)then:customerToCart.get(sampleCustomer) == sampleCartand:sampleCustomer.setEmail("email@customer.com")customerToCart.get(sampleCustomer) == sampleCart }在上述測試中,我們希望確保在更改示例客戶的電子郵件后,我們仍然能夠找到其購物車。 不幸的是,該測試失敗。 為什么? 因為HashMap將密鑰存儲在“存儲桶”中。 每個存儲桶都具有特定范圍的哈希。 由于這個想法,哈希映射非常快。 但是,如果我們將密鑰存儲在第一個存儲桶中(負責1到10之間的散列),然后hashCode方法的值返回11而不是5(因為它是可變的),會發生什么? 哈希圖嘗試查找密鑰,但是它檢查第二個存儲桶(保留哈希11到20)。 它是空的。 因此,對于給定的客戶根本沒有購物車。 這就是為什么擁有不可更改的哈希碼如此重要的原因!
實現它的最簡單方法是使用不可變對象。 如果由于某種原因在您的實現中不可能,那么請記住將hashCode方法限制為僅使用對象的不可變元素。
第二個hashCode規則告訴我們,如果兩個對象相等(根據equals方法),則哈希值必須相同。 這意味著我必須將這兩種方法相關聯,這可以通過基于相同的信息(基本上是字段)來實現。
最后但并非最不重要的一點是,它告訴我們有關等式的傳遞性。 它看起來很瑣碎,但事實并非如此-至少在您考慮繼承時。 想象我們有一個擴展了日期時間對象的日期對象。 為一個日期實現equals方法很容易–當兩個日期相同時,我們返回true。 日期時間也一樣。 但是,當我想將日期與日期時間進行比較時會發生什么? 他們有相同的日期,月份和年份是否足夠? 是否可以比較小時和分鐘,因為日期上沒有此信息? 如果我們決定使用這種方法,那我們就搞砸了。 請分析以下示例:
2016-11-28 == 2016-11-28 12:202016-11-28 == 2016-11-28 15:52由于equals的傳遞性,我們可以說2016-11-28 12:20等于2016-11-28 15:52這當然是愚蠢的。 但是,當您考慮平等合同時是正確的。
JPA用例
不讓我們談論JPA。 看起來在這里實現equals和hashCode方法非常簡單。 我們對每個實體都有唯一的主鍵,因此基于此信息的實現是正確的。 但是,何時分配了該唯一ID? 在對象創建期間還是在刷新更改到數據庫之后? 如果您是手動分配ID,則可以,但是如果您依賴底層引擎,則可能會陷入陷阱。 想象這樣的情況:
public class Customer {@OneToMany(cascade = CascadeType.PERSIST)private Setaddresses = new HashSet<>();public void addAddress(Address newAddress) {addresses.add(newAddress);}public boolean containsAddress(Address address) {return addresses.contains(address);} }如果地址的hashCode基于ID,則在保存Customer實體之前,我們可以假定所有哈希碼均等于零(因為還沒有ID)。 刷新更改后,將分配ID,這也會導致新的哈希碼值。 現在,您可以調用containsAddress方法,不幸的是,由于與在第一部分中討論HashMap的相同原因,它將始終返回false。 我們如何保護這種問題? 據我所知,有一種有效的解決方案– UUID。
class Address {@Id@GeneratedValueprivate Long id;private UUID uuid = UUID.randomUUID();// all other fields with getters and setters if you need@Overridepublic boolean equals(final Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass()) return false;final Address address = (Address) o;return Objects.equals(uuid, address.uuid);}@Overridepublic int hashCode() {return Objects.hash(uuid);} }uuid字段(可以是UUID或簡單地為String)在對象創建期間分配,并在整個實體生命周期中保持不變。 它存儲在數據庫中,并在查詢該對象后立即加載到字段中。 它或當然會增加一些開銷和占用空間,但沒有免費的東西。 如果您想了解有關UUID方法的更多信息,可以查看有關此內容的兩篇精彩文章:
- https://www.percona.com/blog/2014/12/19/store-uuid-optimized-way/
- https://vladmihalcea.com/2014/07/01/hibernate-and-uuid-identifiers/
偏向鎖定
十多年來,Java中的默認鎖定實現使用一種稱為“偏置鎖定”的東西。 可以在標志注釋中找到有關此技術的簡要信息(來源: Java Tuning White Paper ):
-XX:+ UseBiasedLocking
啟用一種用于提高無競爭同步性能的技術。 一個對象被“偏向”線程,該線程首先通過Monitorenter字節碼或同步方法調用來獲取其監視器。 在多處理器計算機上,該線程執行的后續與監視器相關的操作相對要快得多。 在啟用了此標志的情況下,某些具有大量無競爭同步的應用程序可能會實現明顯的加速。 盡管已嘗試將負面影響降到最低,但某些具有某些鎖定模式的應用程序可能會變慢。
對于我們而言,有關此帖子的有趣之處是內部如何實現偏置鎖定。 Java使用對象標頭存儲持有鎖的線程的ID。 問題在于對象標頭的布局定義明確(如果您有興趣,請參閱OpenJDK源hotspot / src / share / vm / oops / markOop.hpp ),不能像這樣“擴展”它。 在64位中,JVM線程ID的長度為54位,因此我們必須決定是否要保留此ID或其他。 不幸的是,“其他”意味著對象哈希碼(實際上是身份哈希碼,存儲在對象頭中)。
每當您對自Object類以來沒有覆蓋它的任何對象調用hashCode()方法時,或者當您直接調用System.identityHashCode()方法時,都將使用此值。 這意味著當您檢索任何對象的默認哈希碼時; 您禁用對此對象的偏向鎖定支持。 這很容易證明。 看一下這樣的代碼:
class BiasedHashCode {public static void main(String[] args) {Locker locker = new Locker();locker.lockMe();locker.hashCode();}static class Locker {synchronized void lockMe() {// do nothing}@Overridepublic int hashCode() {return 1;}} }當您使用以下VM標志運行main方法時: -XX:BiasedLockingStartupDelay=0 -XX:+TraceBiasedLocking您會看到……沒有什么有趣的事情:)
但是,從Locker類中刪除hashCode實現后,情況將發生變化。 現在我們可以在日志中找到這樣的行:
Revoking bias of object 0x000000076d2ca7e0 , mark 0x00007ff83800a805 , type BiasedHashCode$Locker , prototype header 0x0000000000000005 , allow rebias 0 , requesting thread 0x00007ff83800a800
為什么會發生? 因為我們要求提供身份哈希碼。 總結一下這一部分:類中沒有hashCode意味著沒有偏向鎖定。
非常感謝https://www.sitepoint.com/java/的 Nicolai Parlog審閱了這篇文章并指出了一些錯誤。
翻譯自: https://www.javacodegeeks.com/2016/12/care-equals-hashcode.html
總結
以上是生活随笔為你收集整理的你为什么要关心equals和hashcode的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 亚马逊将向美国人工智能企业Anthrop
- 下一篇: 查看进程linux命令 pid(查看进程