hashCode和equals是Object的非final方法,它的存在就是用來被重寫的。
Object的equals方法如下:
public boolean equals(Object obj) {
return (this == obj);
}
Object的hashCode方法是個native方法。
hashCode返回對象的hash值,主要用戶快速查找。在HashMap,HashTable這類散列數據結構中,都是通過hashCode方法查找對象在散列表中的位置。
什麼情況下需要重寫
equals
Object的equals是用==來判斷兩個對象是否是同一個對象,及對象地址是否相等。而重寫equals爲了達到的目的就是實現對象邏輯相等,即值相等。
所以,如果是如下幾種情況,不需要重寫equals方法:
- 強調活動實體,而不關心值。比如Thread,我們只在乎是哪個線程,不在乎線程的值,所以只需要比較地址就好。
- 不存在邏輯相等。不會用到比較值的功能,所以可以不重寫。
- 父類已經重寫了equals,子類只需要用父類重寫的equals方法即可。
hashCode
如果重寫了equals方法,此時必須重寫hashCode方法。
爲什麼?
因爲關於hashCode和equals有這樣的規範:
- 如果兩個對象hashCode相等,對象不一定相等(hash衝突的情況);
- 如果兩個對象equals方法返回true,hashCode必相等。
如果我們只重寫了equals沒有重寫hashCode,就可能違反上述第2條規範。違反會造成什麼問題呢?在HashMap、HashSet一類散列集合中,會根據對象的hashCode先找對象位置,再根據對象的equals方法判斷是否是同一個值。我們存兩個equals方法返回true的對象到集合,如果我們沒有滿足第2條規範,就會導致集合中存在兩個相同,但是hashCode不同對象,不是散列集合想達到的目的。
怎麼寫
equals重寫準則
- 自反性:對於任何非空引用值x,x.equals(x)都應該返回true。
- 對稱性:對於任何非空引用值x、y,當且僅當x.equals(y)返回true時,y.equals(x)才返回true。
- 傳遞性:對於任何非空引用值x、y、z,如果x.equals(y)返回true,且y.equals(z)返回true,則x.equals(z)也返回true。
- 一致性:對於任何非空引用值,如果值沒有被修改,則無論調用多少次equals方法結果始終相等。
- 非空性:對於任何非空引用值x,x.equals(null)都應該返回false。
equals重寫技巧
- 使用==檢查參數是否爲這個對象的引用:如果是本身,則直接返回,優化性能。
- 使用instanceOf檢查參數類型是否正確:如果不是,直接返回false。如果兼容不同類型,很容易違反對稱性。
- 將參數類型強制轉換爲正確的類型:第2步保證了該步不會拋出異常。
- 對於該類中的“關鍵域”,檢查參數中的域是否與對象中的對應域相等:基本類型的域就用==比較,float域用Float.compare方法,double域用Double.compare方法,至於別的引用域,我們一般遞歸調用它們的equals方法比較,加上判空檢查和對自身引用的檢查,一般會寫成這樣:(field == o.field || (field != null && field.equals(o.field))),而上面的String裏使用的是數組,所以只要把數組中的每一位拿出來比較就可以了。
- 編寫完成後思考是否滿足上面提到的對稱性,傳遞性,一致性等等。
hashCode重寫技巧
兩個目標,一個爲不同的目標生成不同的散列值,一個是把實例均勻分佈在所有的散列值上。
引自Effective Java
- 把某個非零的常數值,比如17,保存在一個int型的result中;
- 對於每個關鍵域f(equals方法中設計到的每個域),作以下操作:
(1)爲該域計算int類型的散列碼:
i.如果該域是boolean類型,則計算(f?1:0),
ii.如果該域是byte,char,short或者int類型,計算(int)f,
iii.如果是long類型,計算(int)(f^(f>>>32)).
iv.如果是float類型,計算Float.floatToIntBits(f).
v.如果是double類型,計算Double.doubleToLongBits(f),然後再計算long型的hash值
vi.如果是對象引用,則遞歸的調用域的hashCode,如果是更復雜的比較,則需要爲這個域計算一個範式,然後針對範式調用hashCode,如果爲null,返回0
vii. 如果是一個數組,則把每一個元素當成一個單獨的域來處理。
(2)result = 31 * result + c。
★ 爲什麼採用31result + c,乘法使hash值依賴於域的順序,如果沒有乘法那麼所有順序不同的字符串String對象都會有一樣的hash值,而31是一個奇素數,如果是偶數,並且乘法溢出的話,信息會丟失,31有個很好的特性是31i ==(i<<5)-i,即2的5次方減1,虛擬機會優化乘法操作爲移位操作的。 - 返回result
- 編寫單元測試驗證有沒有實現所有相等的實例都有相等的散列碼。