java~重(zhong)寫hashcode和equals
一 單字段和多字段重寫hashcode
在 Java 中,重寫 hashCode 方法的場景通常與對象的哈希值計算有關,特別是在使用哈希表(如 HashMap, HashSet 等)時。下面是你提供的兩種 hashCode 實現的(de)具體使用(yong)場景分(fen)析:
1. 第一種實現
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
DefaultClientScopeRealmMappingEntity.Key key = (DefaultClientScopeRealmMappingEntity.Key) o;
if (clientScopeId != null ? !clientScopeId.equals(key.getClientScopeId() != null ? key.getClientScopeId() : null) : key.getClientScopeId() != null) return false;
if (realm != null ? !realm.getId().equals(key.realm != null ? key.realm.getId() : null) : key.realm != null) return false;
return true;
}
@Override
public int hashCode() {
int result = clientScopeId != null ? clientScopeId.hashCode() : 0;
result = 31 * result + (realm != null ? realm.getId().hashCode() : 0);
return result;
}
使用場景:
- 多字段組合:當一個對象由多個字段組成且這些字段共同決定對象的唯一性時,這種方式非常合適。在這個例子中,
clientScopeId和realm.getId()兩個字段共同影響對象的哈希值。 - 確保一致性:如果
clientScopeId和realm是對象的重要屬性,并且它們的值會影響對象的相等性(即equals方法),則需要根據這些字段來計算哈希值,以確保在集合中正確地存儲和查找對象。 - 避免哈希沖突:通過將多個字段結合起來計算哈希值,可以降低不同對象之間的哈希沖突概率,提高性能。
2. 第二種實現
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null) return false;
if (!(o instanceof CredentialEntity)) return false;
CredentialEntity that = (CredentialEntity) o;
if (!id.equals(that.getId())) return false;
return true;
}
@Override
public int hashCode() {
return id.hashCode();
}
使用場景:
- 單一標識符:當對象可以用單一字段(如
id)唯一標識時,這種實現方式更加簡潔有效。如果id是對象的唯一標識符,那么直接使用id的哈希值是合理的。 - 簡單性:這種實現較為簡單,易于理解,適用于那些不需要考慮多個字段組合的情況。
- 性能優化:由于只計算一個字段的哈希值,性能開銷較小,適合對性能要求較高的場景。
總結
- 選擇第一種實現:適用于包含多個重要字段的復雜對象,確保對象在集合中的正確性和唯一性。
- 選擇第二種實現:適用于簡單對象,僅依賴于一個唯一標識符,代碼更簡潔且性能較好。
在實際開發中,選擇哪種實現應依據對象的設計及其在數據結構中的使用方式。確保 hashCode 和 equals 方法(fa)的(de)(de)一致性是非常重要的(de)(de),以避免潛在(zai)的(de)(de)錯(cuo)誤。
二 hashCode 方法和 equals 方法的不一致時的問題
在 Java 中,hashCode 方法和 equals 方法的不一致性會導致一系列問題,特別是在使用哈希表(如 HashMap, HashSet 等)時。以下是(shi)一些主要(yao)的問題(ti):
1. 數據丟失
- 無法查找:如果兩個對象被認為相等(即
equals返回true),但它們的哈希碼不同(即hashCode返回不同的值),則它們可能會被存儲在哈希表中的不同桶中。這意味著你無法通過一個對象找到另一個對象,從而導致數據丟失。
2. 錯誤的集合行為
- 重復元素:在
HashSet中,如果兩個對象的equals方法返回true,則不應允許將其作為重復元素添加。如果hashCode不一致,可能會導致集合中出現多個看似相同的元素。 - 錯誤的刪除操作:當從集合中刪除一個對象時,如果
hashCode不一致,可能會導致無法正確找到并刪除該對象。
3. 性能問題
- 性能下降:不一致的
hashCode和equals實現會導致哈希表中的鏈表變長,從而影響查找和插入操作的性能。這使得哈希表的平均時間復雜度從 O(1) 降低到 O(n)。
4. 難以調試
- 邏輯錯誤:由于不一致性,程序的行為可能與預期不符,這使得調試變得更加困難。開發者可能難以追蹤問題的根源,因為錯誤可能在于對象的比較和哈希計算。
5. 違反合同
- 違反 Java 合同:Java 文檔明確規定,如果兩個對象相等(
a.equals(b)為true),那么它們的哈希碼必須相等(a.hashCode() == b.hashCode())。不遵循這一規則會導致程序行為不可預測,甚至引發異常。
結論
為了避免上述問題,確保在重寫 equals 方法時也相應地重寫 hashCode 方法(fa),并且要保證它(ta)們(men)之間的(de)一致(zhi)性。通常的(de)做(zuo)法(fa)是:
- 如果兩個對象相等(
equals返回true),那么它們的hashCode必須相等。 - 如果兩個對象的
hashCode相等,則它們不一定相等,但如果相等,則應返回true。
三 重寫equals的約定【參考EffectiveJava】
在(zai)覆(fu)(fu)蓋(gai)了(le) equals 方(fang)法(fa)的類中,必須覆(fu)(fu)蓋(gai) hashCode 方(fang)法(fa)。 如果你沒有這(zhe)樣做,該類將違反 hashCode 方(fang)法(fa)的一般約定(ding),這(zhe)將阻止該類在(zai) HashMap 和 HashSet 等集(ji)合中正常運行。以下是根(gen)據 Object 規范修改的約定(ding):
- 應用程序執行期間對對象重復調用 hashCode 方法時,它必須一致地返回相同的值,前提是不對 equals 方法中用于比較的信息進行修改。這個值不需要在應用程序的不同執行之間保持一致。
- 如果根據 equals(Object) 方法判斷出兩個對象是相等的,那么在兩個對象上調用 hashCode 方法必須產生相同的整數結果
- 如果根據 equals(Object) 方法判斷出兩個對象不相等,則不需要在每個對象上調用 hashCode 方法時必須產生不同的結果。但是,程序員應該知道,為不相等的對象生成不同的結果可能會提高散列表的性能。
當你無法覆蓋 hashCode 方法時,將違反第二個關鍵條款:相等的對象必須具有相等的散列碼。 根據類的(de)(de) equals 方法,兩個不(bu)同的(de)(de)實例在邏輯上(shang)可(ke)能是相等的(de)(de),但是對(dui)(dui)于對(dui)(dui)象的(de)(de) hashCode 方法來說,它們只是兩個沒有共同之(zhi)處(chu)的(de)(de)對(dui)(dui)象。因此(ci),Object 的(de)(de) hashCode 方法返回(hui)兩個看似隨機的(de)(de)數(shu)字(zi),而(er)不(bu)是約定要求的(de)(de)兩個相等的(de)(de)數(shu)字(zi)。例如(ru),假(jia)設你(ni)嘗試(shi)使用Item-10中的(de)(de) PhoneNumber 類實例作(zuo)為 HashMap 中的(de)(de)鍵:
Map<PhoneNumber, String> m = new HashMap<>();
m.put(new PhoneNumber(707, 867, 5309), "Jenny");
此時,你(ni)可能(neng)(neng)期望 m.get(new PhoneNumber(707, 867,5309)) 返(fan)回「Jenny」,但(dan)是它(ta)返(fan)回 null。注意,這(zhe)(zhe)里涉及到兩(liang)個 PhoneNumber 實例:一(yi)個用于(yu)插入到 HashMap 中,另一(yi)個相等的(de)(de)實例(被(bei)試圖)用于(yu)檢(jian)索。由于(yu) PhoneNumber 類(lei)未(wei)能(neng)(neng)覆(fu)蓋 hashCode 方法,導致兩(liang)個相等的(de)(de)實例具(ju)有(you)不(bu)(bu)相等的(de)(de)散(san)列(lie)(lie)碼(ma),這(zhe)(zhe)違反了 hashCode 方法約定。因(yin)此,get 方法查(cha)(cha)找(zhao)電(dian)(dian)話(hua)號(hao)碼(ma)的(de)(de)散(san)列(lie)(lie)桶可能(neng)(neng)會與 put 方法存儲(chu)電(dian)(dian)話(hua)號(hao)碼(ma)的(de)(de)散(san)列(lie)(lie)桶不(bu)(bu)同。即使這(zhe)(zhe)兩(liang)個實例碰(peng)巧分配在同一(yi)個散(san)列(lie)(lie)桶上,get 方法幾乎肯定會返(fan)回 null,因(yin)為 HashMap 有(you)一(yi)個優化(hua),它(ta)緩存每(mei)個條目相關聯(lian)的(de)(de)散(san)列(lie)(lie)碼(ma),如果散(san)列(lie)(lie)碼(ma)不(bu)(bu)匹配,就不(bu)(bu)會檢(jian)查(cha)(cha)對(dui)象(xiang)是否相等。
解決這個問題就像(xiang)為(wei) PhoneNumber 編寫一(yi)個正確(que)的 hashCode 方法(fa)(fa)(fa)一(yi)樣簡單。那(nei)么(me) hashCode 方法(fa)(fa)(fa)應該是什(shen)么(me)樣的呢?寫一(yi)個反(fan)面例子很容易。例如,以(yi)下方法(fa)(fa)(fa)是合法(fa)(fa)(fa)的,但是不應該被使用:
// The worst possible legal hashCode implementation - never use!
@Override
public int hashCode() { return 42; }
它是合法(fa)的(de)(de),因(yin)為它確保了相(xiang)等的(de)(de)對(dui)象(xiang)(xiang)具有相(xiang)同(tong)的(de)(de)散(san)列(lie)碼(ma)。同(tong)時它也很糟糕,因(yin)為它使(shi)每個(ge)對(dui)象(xiang)(xiang)都有相(xiang)同(tong)的(de)(de)散(san)列(lie)碼(ma)。因(yin)此,每個(ge)對(dui)象(xiang)(xiang)都分配(pei)到同(tong)一個(ge)桶中(zhong),散(san)列(lie)表退化(hua)為鏈表。這(zhe)樣,原本應該在線(xian)性階 O(n) 運行的(de)(de)程序將在平方階 O(n^2) 運行。對(dui)于大型散(san)列(lie)表,這(zhe)是工作和(he)不工作的(de)(de)區別。
一個好的散列算法傾向于為不相等的實例生成不相等的散
列(lie)碼。這正是(shi) hashCode 方法(fa)約定第三部分的含(han)義(yi)。理想(xiang)情況下,一個散列(lie)算法(fa)應該在所有(you) int 值上均勻合理分布所有(you)不相等實(shi)(shi)例(li)集合。實(shi)(shi)現這個理想(xiang)是(shi)很困難的。幸運的是(shi),實(shi)(shi)現一個類似的并不太難。這里有(you)一個簡單的方式:
- 聲明一個名為 result 的 int 變量,并將其初始化為對象中第一個重要字段的散列碼 c,如步驟 2.a 中計算的那樣。(回想一下 Item-10 中的重要字段會對比較產生影響)
- 對象中剩余的重要字段 f,執行以下操作:
- 為字段計算一個整數散列碼 c:
- 如果字段是基本數據類型,計算 Type.hashCode(f),其中 type 是與 f 類型對應的包裝類。
- 如果字段是對象引用,并且該類的 equals 方法通過遞歸調用 equals 方法來比較字段,則遞歸調用字段上的 hashCode 方法。如果需要更復雜的比較,則為該字段計算一個「canonical representation」,并在 canonical representation 上調用 hashCode 方法。如果字段的值為空,則使用 0(或其他常數,但 0 是慣用的)。
- 如果字段是一個數組,則將其每個重要元素都視為一個單獨的字段。也就是說,通過遞歸地應用這些規則計算每個重要元素的散列碼,并將每個步驟 2.b 的值組合起來。如果數組中沒有重要元素,則使用常量,最好不是 0。如果所有元素都很重要,那么使用 Arrays.hashCode。
- 將步驟 2.a 中計算的散列碼 c 合并到 result 變量,如下所示:
result = 31 * result + c;
- 返回 result 變量。
當你完成了(le) hashCode 方(fang)法(fa)的編寫之后(hou),問問自己現在(zai)相(xiang)同的實(shi)例是否具有相(xiang)同的散列碼(ma)。編寫單元測試來驗證你的直覺(除非你使用 AutoValue 生成你的 equals 方(fang)法(fa)和(he) hashCode 方(fang)法(fa),在(zai)這種情(qing)況下你可以安全地省略這些測試)。如果(guo)相(xiang)同的實(shi)例有不相(xiang)等的散列碼(ma),找出原因(yin)并修復問題。
可以(yi)從(cong)散列碼計算中(zhong)排(pai)除(chu)派生字(zi)段(duan)。換句話說,你(ni)可以(yi)忽略(lve)任何可以(yi)從(cong)包含(han)的字(zi)段(duan)計算其值的字(zi)段(duan)。你(ni)必(bi)須排(pai)除(chu)不用(yong) equals 比較的任何字(zi)段(duan),否(fou)則你(ni)可能會違反(fan) hashCode 方法約定的第二個條款。
在步驟(zou) 2.b 中(zhong)使(shi)用的(de)(de)乘法(fa)將(jiang)使(shi)結果取決于字(zi)段(duan)的(de)(de)順序,如(ru)果類有(you)多個相(xiang)似的(de)(de)字(zi)段(duan),則會(hui)產生一(yi)個更(geng)(geng)好的(de)(de)散(san)列(lie)(lie)算法(fa)。例(li)如(ru),如(ru)果字(zi)符串(chuan)散(san)列(lie)(lie)算法(fa)中(zhong)省略了乘法(fa),那么所有(you)的(de)(de)字(zi)母順序都有(you)相(xiang)同(tong)的(de)(de)散(san)列(lie)(lie)碼。選(xuan)擇 31 是(shi)因為(wei)它是(shi)奇素(su)數(shu)。如(ru)果是(shi)偶數(shu),乘法(fa)運算就(jiu)會(hui)溢出(chu),信息就(jiu)會(hui)丟(diu)失,因為(wei)乘法(fa)運算等同(tong)于移位(wei)。使(shi)用素(su)數(shu)的(de)(de)好處不太明顯,但(dan)它是(shi)傳統用法(fa)。31 有(you)一(yi)個很好的(de)(de)特性,可以用移位(wei)和減法(fa)來代替乘法(fa),從而在某些體系結構上獲得(de)更(geng)(geng)好的(de)(de)性能(neng):31 * i == (i <<5) – i。現代虛擬(ni)機自動進(jin)行這種優(you)化。
讓(rang)我們將(jiang)前面的方(fang)法應用到 PhoneNumber 類:
// Typical hashCode method
@Override
public int hashCode() {
int result = Short.hashCode(areaCode);
result = 31 * result + Short.hashCode(prefix);
result = 31 * result + Short.hashCode(lineNum);
return result;
}
因為這個方法返回一個簡單的(de)(de)(de)確定的(de)(de)(de)計(ji)算結果,它的(de)(de)(de)唯(wei)一輸(shu)入是 PhoneNumber 實(shi)例中(zhong)的(de)(de)(de)三個重要字(zi)段,所以很(hen)明顯(xian),相等的(de)(de)(de) PhoneNumber 實(shi)例具有相等的(de)(de)(de)散(san)列(lie)碼。實(shi)際上,這個方法是 PhoneNumber 的(de)(de)(de)一個非常好的(de)(de)(de) hashCode 方法實(shi)現(xian),與(yu) Java 庫中(zhong)的(de)(de)(de) hashCode 方法實(shi)現(xian)相當(dang)。它很(hen)簡單,速度也相當(dang)快,并且合理地將不相等的(de)(de)(de)電話號(hao)碼分散(san)到不同的(de)(de)(de)散(san)列(lie)桶(tong)中(zhong)。
雖然本條目中的(de)(de)(de)方法(fa)(fa)產生了(le)相(xiang)當不(bu)錯的(de)(de)(de)散(san)列(lie)算(suan)(suan)法(fa)(fa),但它(ta)們并(bing)不(bu)是最先進的(de)(de)(de)。它(ta)們的(de)(de)(de)質量可與(yu) Java 庫的(de)(de)(de)值類型中的(de)(de)(de)散(san)列(lie)算(suan)(suan)法(fa)(fa)相(xiang)媲美,對于大多數用途來說都是足夠的(de)(de)(de)。如果你確實需要不(bu)太(tai)可能產生沖突的(de)(de)(de)散(san)列(lie)算(suan)(suan)法(fa)(fa),請參(can)閱(yue) Guava 的(de)(de)(de) com.google.common.hash.Hashing [Guava]。
Objects 類有一(yi)個靜態方(fang)(fang)(fang)法,它(ta)接受任意數量(liang)的(de)(de)對象并(bing)返回它(ta)們的(de)(de)散(san)列(lie)碼(ma)。這個名為(wei) hash 的(de)(de)方(fang)(fang)(fang)法允許你編(bian)寫只有一(yi)行代(dai)碼(ma)的(de)(de) hashCode 方(fang)(fang)(fang)法,這些(xie)方(fang)(fang)(fang)法的(de)(de)質量(liang)可(ke)(ke)以與本條目中提供的(de)(de)編(bian)寫方(fang)(fang)(fang)法媲美。不(bu)幸的(de)(de)是,它(ta)們運行得更慢,因為(wei)它(ta)們需要創建(jian)數組來(lai)傳遞可(ke)(ke)變數量(liang)的(de)(de)參(can)數,如(ru)果任何參(can)數是原始類型(xing)的(de)(de),則需要進行裝箱和拆(chai)箱。推薦(jian)只在(zai)性能不(bu)重要的(de)(de)情(qing)況下使用(yong)這種(zhong)散(san)列(lie)算法。下面是使用(yong)這種(zhong)技術編(bian)寫的(de)(de) PhoneNumber 的(de)(de)散(san)列(lie)算法:
// One-line hashCode method - mediocre performance
@Override
public int hashCode() {
return Objects.hash(lineNum, prefix, areaCode);
}
如(ru)(ru)果(guo)一個類是(shi)不可變(bian)的(de)(de),并且計算散(san)(san)列碼(ma)的(de)(de)成本非常(chang)高,那么你(ni)可以考慮在(zai)對(dui)象中緩存散(san)(san)列碼(ma),而不是(shi)在(zai)每次(ci)請求時(shi)(shi)(shi)重新計算它。如(ru)(ru)果(guo)你(ni)認(ren)為(wei)這種類型的(de)(de)大多數對(dui)象都將用(yong)作(zuo)散(san)(san)列鍵,那么你(ni)應該(gai)在(zai)創建實例時(shi)(shi)(shi)計算散(san)(san)列碼(ma)。否則,你(ni)可以選擇(ze)在(zai)第一次(ci)調用(yong) hashCode 方法時(shi)(shi)(shi)延(yan)遲(chi)初始化(hua)散(san)(san)列碼(ma)。在(zai)一個延(yan)遲(chi)初始化(hua)的(de)(de)字(zi)段(duan)(Item-83)的(de)(de)情況下,需要(yao)注意以確保該(gai)類仍然是(shi)線程(cheng)安(an)全的(de)(de)。我們的(de)(de) PhoneNumber 類不值得(de)進行這種處理,但只是(shi)為(wei)了向(xiang)你(ni)展示(shi)它是(shi)如(ru)(ru)何實現的(de)(de),如(ru)(ru)下所(suo)示(shi)。注意,散(san)(san)列字(zi)段(duan)的(de)(de)初始值(在(zai)本例中為(wei) 0)不應該(gai)是(shi)通(tong)常(chang)創建的(de)(de)實例的(de)(de)散(san)(san)列碼(ma):
// hashCode method with lazily initialized cached hash code
private int hashCode; // Automatically initialized to 0
@Override
public int hashCode() {
int result = hashCode;
if (result == 0) {
result = Short.hashCode(areaCode);
result = 31 * result + Short.hashCode(prefix);
result = 31 * result + Short.hashCode(lineNum);
hashCode = result;
}
return result;
}
不要試圖從(cong)散(san)列(lie)(lie)碼計算(suan)中排除(chu)重要字段,以提高性(xing)能。 雖(sui)然得(de)(de)到的(de)散(san)列(lie)(lie)算(suan)法(fa)可能運(yun)行(xing)(xing)得(de)(de)更快,但其糟(zao)糕的(de)質量(liang)可能會將散(san)列(lie)(lie)表的(de)性(xing)能降(jiang)低到無(wu)法(fa)使用(yong)的(de)程度。特別是,散(san)列(lie)(lie)算(suan)法(fa)可能會遇到大量(liang)實例(li),這(zhe)些(xie)實例(li)在你選擇忽略的(de)不同區域。如(ru)果發生(sheng)這(zhe)種情況,散(san)列(lie)(lie)算(suan)法(fa)將把所有這(zhe)些(xie)實例(li)映射很少一部分(fen)散(san)列(lie)(lie)碼,使得(de)(de)原本應(ying)該在線性(xing)階 O(n) 運(yun)行(xing)(xing)的(de)程序將在平方階 O(n^2) 運(yun)行(xing)(xing)。
這不僅僅是一(yi)個(ge)(ge)理論問題。在 Java 2 之(zhi)前,字符(fu)(fu)(fu)串散列(lie)算法在字符(fu)(fu)(fu)串中,以第(di)一(yi)個(ge)(ge)字符(fu)(fu)(fu)開始,最(zui)多使(shi)用 16 個(ge)(ge)字符(fu)(fu)(fu)。對于大量且分層次(ci)的(de)集合(如 url),該函數完全(quan)展示(shi)了前面(mian)描述的(de)病態行(xing)為。
不(bu)要為(wei) hashCode 返回的(de)值(zhi)提供詳細的(de)規范,這樣客戶(hu)端就(jiu)不(bu)能理所應當的(de)依(yi)賴它(ta)。這(也)給了(le)(le)你(ni)更改它(ta)的(de)余地。 Java 庫(ku)中(zhong)的(de)許多類,例(li)如(ru) String 和 Integer,都將 hashCode 方(fang)法返回的(de)確切(qie)值(zhi)指定為(wei)實例(li)值(zhi)的(de)函(han)數。這不(bu)是一個好主意,而是一個我們不(bu)得不(bu)面(mian)對(dui)的(de)錯誤:它(ta)阻礙(ai)了(le)(le)在未來(lai)版本中(zhong)提高散(san)列算(suan)法的(de)能力。如(ru)果你(ni)保留(liu)了(le)(le)未指定的(de)細節,并且在散(san)列算(suan)法中(zhong)發(fa)現了(le)(le)缺陷,或者(zhe)發(fa)現了(le)(le)更好的(de)散(san)列算(suan)法,那么(me)你(ni)可以在后(hou)續版本中(zhong)更改它(ta)。
總之,每次覆(fu)蓋 equals 方(fang)(fang)法時都必(bi)須覆(fu)蓋 hashCode 方(fang)(fang)法,否則(ze)程序(xu)將無(wu)法正(zheng)確運行。你的(de) hashCode 方(fang)(fang)法必(bi)須遵守 Object 中指定的(de)通用約(yue)定,并且必(bi)須合(he)理地將不相等(deng)的(de)散列碼(ma)分配給不相等(deng)的(de)實例。這很容易實現,如果(guo)有點枯燥,可使用第 51 頁(ye)的(de)方(fang)(fang)法。如 Item-10 所述,AutoValue 框架提供(gong)了一種能(neng)很好的(de)替代手動(dong)編寫 equals 方(fang)(fang)法和 hashCode 方(fang)(fang)法的(de)功能(neng),IDE 也提供(gong)了這種功能(neng)。