這是一篇關于hashCode方法,可變對象和內存泄漏問題的文章。
1. 重寫 hashCode() 和 equals() 的契約
每個 JAVA 對象都有兩個非常重要的方法,比如 hashCode() 和 equals() 方法。這些方法旨在根據其特定的一般規則進行重寫。本文描述了為什么以及如何覆蓋 hashCode() 方法,該方法在使用 HashMap , HashSet 或任何 Collection 時保留 HashCode 的契約。
1.1 hashCode 契約
hashCode 的契約就是:
如果兩個對象相等,那么調用兩個對象的 hashCode() 方法一定會返回相同的 hash 值。
現在你應該想到的問題是:上述陳述是否應該永遠是真的?
考慮一下這樣一個事實,當我們為我們的類提供了一個正確的 equals 實現,那么如果我們不遵守上述規則會發生什么。
為了回答上面的問題,我們來考慮兩個問題:
- 對象是相等的,但是返回了不同的 hashCode
- 對象不是相等的,但是它們卻有相同的 hashCode
1.1.1 對象是相等的,但是返回了不同的 hashCode
當兩個對象是相等的,但是返回了不同的 hashCode 會發生什么?你的代碼會運行的很好。除非你沒有將對象存儲在像 HashSet 或 HashMap 這樣的集合中,否則永遠不會遇到麻煩。但是當你將你的對象存儲到上面提到的那種集合中的時候,在運行的時候可能會發生一些奇怪的問題。
為了更好的理解這個問題,你必須理解集合類中像 hashMap , HashSet 這樣的數據結構是如何工作的。這些集合類取決于您作為其中的鍵放置的對象,且必須遵守上述契約的事實。如果你沒有遵循上面的契約, 并且嘗試將對象存儲在集合中,那么在運行期你將會得到一個奇怪并且不可預料的結果。
以 HashMap 為例子來說明。當你在 hashMap 中存儲值的時候,這些值實際存儲在一組桶中。每個桶都分配了一個用于識別它的號碼。當你在 HashMap 中 put 一個值的時候,它就會在那些桶中存儲數據。具體存儲在哪個桶中,取決于你的對象所返回的 hashcode 。換句話說,如果一個對象調用 hashCode() 方法返回了49,那么它就會存儲在 HashMap 編號為49的這個桶中。
隨后,當你嘗試通過調用 contains(element) 方法去檢查集合中是否包含該元素。HashMap 首先會得到這個 element 的 hashCode, 然后,它將查看與 hashCode 對應的存儲桶。如果存儲桶為空,則表示我們已完成,并且返回 false ,這意味著 HashMap 不包含該元素。
如果存儲桶中有一個或多個對象,則它將使用您定義的 equals() 函數將 element 與該存儲桶中的所有其他元素進行比較。
1.1.2 對象不是相等的,但是它們卻有相同的 hashCode
hashCode 契約沒有說明上述語句。因此,不同的對象可能返回相同的 hashCode 值, 但是如果不同的對象返回相同的 hashCode 值,則像 HashMap 這樣的集合將無法正常使用。
1.2 為什么是存儲桶?
您可以想象,如果放在 HashMap 中的所有對象都存儲在一個大列表中,那么當您想要檢查特定元素是否在 Map 中時, 您必須將輸入與列表中的所有對象進行比較。通過使用存儲桶,您現在只比較特定存儲桶的元素, 并且任何存儲桶通常只包含 HashMap 中所有元素的一小部分。
1.3 重寫 hashCode 方法
編寫一個好的 hashCode() 方法對于新類來說總是一項棘手的任務。
1.3.1 返回固定的值
您可以實現 hashCode() 方法,以便始終返回固定值,例如:
//bad performance @Override public int hashCode() { return 1; }
上述方法滿足所有要求,并根據哈希碼合同被認為是合法的,但效率不高。如果使用此方法,則所有對象將存儲在同一個存儲桶(即存儲桶1)中,當您嘗試確保特定對象是否存在于集合中時,它始終必須檢查集合的整個內容。另一方面,如果為您的類重寫 hashCode() 方法,并且該方法違反了契約,則調用 contains() 方法可能會對集合中存在但在另一個存儲桶中的元素返回 false 。
1.3.2 Effective Java中的方法
Joshua Bloch 在 Effective Java 中提供了一個生成 hashCode() 值的指導方法:
- 存儲一些常量非零值;比方說17,在一個名為 result 的 int 變量中。
- 對于對象中的每個重要字段 f ( equals() 考慮的每個字段),請執行以下操作:
a. 為字段 c 計算一個 int 類型的 hashCode ;
i. 如果值域是一個布爾類型值,計算 c=(f?1:0)
ii. 如果域是一個 byte , char , short , int ,計算 c=(int)f
iii.如果域是一個 long 類型,計算 c=(int)(f^(f>>>32)).
iv.如果域是一個 float 類型,計算 c=Float.floatToIntBits(f).
v.如果域是一個 double 類型,計算 long l = Double.doubleToLongBits(f) , c = (int)(l^(l>>>32))
vi.如果該字段是對象引用,則 equals() 為該字段調用 equals() 。計算 c = f.hashCode()
vii.如果域是一個數組,將其視為每個元素都是一個單獨的字段。
也就是說,通過將上述規則應用于每個元素來為每個重要元素計算 hashCode。
b.將步驟2.a中計算的 hashCode c 組合到結果中,如下所示:result = 37 * result + c;
- 返回結果值
- 查看生成的 hashCode() 并確保相等的實例具有相同的哈希碼。
以下是遵循上述準則的類的示例
public class HashTest { private String field1; private short field2; @Override public int hashCode() { int result = 17; result = 37*result + field1.hashCode(); result = 37*result + (int)field2; return result; } }
您可以看到選擇常數37。選擇這個數字的目的是它是一個素數。我們可以選擇任何其他素數。
1.3.3 Apache HashCodeBuilder
編寫好的 hashCode() 方法并不總是那么容易。由于正確實現 hashCode() 可能很困難,如果我們有一些可重用的實現,將會很有幫助。Jakarta-Commonsorg.apache.commons.lang.builder 包提供了一個名為 HashCodeBuilder 的類,旨在幫助實現 hashCode()方法。通常,開發人員很難實現 hashCode() 方法,這個類旨在簡化流程。
以下是為上述類實現 hashCode 算法的方法:
public class HashTest { private String field1; private short field2; @Override public int hashCode() { return new HashCodeBuilder(83, 7) .Append(field1) .append(field2) .toHashCode(); } }
請注意,構造函數的兩個數字只是兩個不同的非零奇數 - 這些數字有助于避免跨對象的 hashCode 值的沖突。
如果需要,可以使用 appendSuper(int) 添加超類 hashCode() 。
您可以看到使用 Apache HashCodeBuilder 重寫 HashCode() 是多么容易。
1.4 可變對象作為 key
一般建議您應該使用不可變對象作為 Collection 中的鍵。從不可變數據計算時, HashCode 效果最佳。如果您使用可變對象作為鍵并更改對象的狀態以便hashCode更改,那么存儲對象將位于 Collection 中的錯誤存儲桶中。在實現 hashCode() 時,您應該考慮的最重要的事情是,無論何時調用此方法,它都應該在每次調用時為特定對象生成相同的值。如果你有一個類似于一個對象的場景,當它被 put() 到一個HaspMap并在 get() 期間產生另一個值時會產生一個 hashCode()值, 在這種情況下,你將無法檢索該對象。
因此,如果您的 hashCode() 依賴于對象中的可變數據,那么通過生成不同的 hashCode(),更改這些數據肯定會產生不同的密鑰。
看下面的例子:
public class Employee { private String name; private int age; public Employee() { } public Employee(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public boolean equals(Object obj) { //Remember: Some Java gurus recommend you avoid using instanceof if (obj instanceof Employee) { Employee emp = (Employee)obj; return (emp.name == name && emp.age == age); } return false; } @Override public int hashCode() { return name.length() + age; } public static void main(String[] args) { Employee e = new Employee("muhammad", 24); Map<Object, Object> m = new HashMap<Object, Object>(); m.put(e, "Muhammad Ali Khojaye"); // getting output System.out.println(m.get(e)); e.name = "abid"; // it fails to get System.out.println(m.get(e)); e.name = "amirrana"; // it fails again System.out.println(m.get(new Employee("muhammad", 24))); } }
因此,您可以在上面的示例中看到我們如何獲得一些不可預測的結果。您可以使用 Joshua Recipe 或使用 HashCodeBuilder 類重寫 hashCode() 來輕松修復上述問題。
這是一個例子:
1.4.1 示例建議:
@Override public int hashCode() { int result = 17; result = 37*result + name.hashCode(); result = 37*result + age; return result; }
1.4.2 使用HashCodeBuilder
@Override public int hashCode() { return new HashCodeBuilder(83, 7).append(name).append(age).toHashCode(); }
1.4.3 可變字段作為鍵的另外一個例子
讓我們來看一下這個例子:
public class HashTest { private int mutableField; private final int immutableField; public HashTest(int mutableField, int immutableField) { this.mutableField = mutableField; this.immutableField = immutableField; } public void setMutableField(int mutableField) { this.mutableField = mutableField; } @Override public boolean equals(Object o) { if(o instanceof HashTest) { return (mutableField == ((HashTest)o).mutableField) && (immutableField == ((HashTest)o).immutableField); }else { return false; } } @Override public int hashCode() { int result = 17; result = 37 * result + this.mutableField; result = 37 * result + this.immutableField; return result; } public static void main(String[] args) { Set<HashTest> set = new HashSet<HashTest>(); HashTest obj = new HashTest(6622458, 626304); set.add(obj); System.out.println(set.contains(obj)); obj.setMutableField(3867602); System.out.println(set.contains(obj)); } }
更改可變字段后,計算出的 hashCode 不再指向舊存儲桶,而 contains() 返回 false. 我們可以使用這些方法中的任何一種來解決這種情況.
- 從不可變數據計算時,Hashcode 是最佳的;因此,請確保只有不可變對象才能用作 Collections 的鍵。
- 使用我們的第一種技術實現 hashCode() ,即返回一個常量值但你必須意識到它會殺死桶機制的所有優點。
- 如果你需要 hashCode 方法中包含的可變字段,那么你可以在創建對象時計算和存儲哈希值,每當你更新可變字段時,你必須先從集合中刪除它( set / map ),然后將它添加回 更新后的集合。
1.5 內存泄漏與HashCode和Equal
如果未實現 equals() 和 hashcode() ,則 Java 應用程序中可能會發生內存泄漏。考慮下面的一個小代碼示例,其中如果未實現 equals() 和 hashcode() ,則 HashMap 保持引用處于活動狀態。結果, HashMap 通過重復添加相同的鍵而不斷增長,最后拋出 OutOfMemoryError 。
public class HashcodeLeakExample { private String id; public HashcodeLeakExample(String id) { this.id = id; } public static void main(String args[]) { try { Map<HashcodeLeakExample, String> map = new HashMap<HashcodeLeakExample, String>(); while (true) { map.put(new HashcodeLeakExample("id"), "any value"); } } catch (Exception ex) { ex.printStackTrace(); } } }
來源:公眾號「鍋外的大佬」