equals和hashCode都是Object物件中的非final方法,它們設計的目的就是被用來覆蓋(override)的,所以在程式設計中還是經常需要處理這兩個方法的。而掌握這兩個方法的覆蓋準則以及它們的區別還是很必要的,相關問題也不少。
下面我們繼續以一次面試的問答,來考察對equals和hashCode的掌握情況。
面試官: Java裡面有==
運算子了,為什麼還需要equals啊?
equals()的作用是用來判斷兩個物件是否相等,在Object裡面的定義是:
public boolean equals(Object obj) {
return (this == obj);
}
複製程式碼
這說明在我們實現自己的equals方法之前,equals等價於==
,而==
運算子是判斷兩個物件是不是同一個物件,即他們的地址是否相等。而覆寫equals更多的是追求兩個物件在邏輯上的相等,你可以說是值相等,也可說是內容相等。
在以下幾種條件中,不覆寫equals就能達到目的:
- 類的每個例項本質上是唯一的:強調活動實體的而不關心值得,比如Thread,我們在乎的是哪一個執行緒,這時候用equals就可以比較了。
- 不關心類是否提供了邏輯相等的測試功能:有的類的使用者不會用到它的比較值得功能,比如Random類,基本沒人會去比較兩個隨機值吧
- 超類已經覆蓋了equals,子類也只需要用到超類的行為:比如AbstractMap裡已經覆寫了equals,那麼繼承的子類行為上也就需要這個功能,那也不需要再實現了。
- 類是私有的或者包級私有的,那也用不到equals方法:這時候需要覆寫equals方法來禁用它:
@Override public boolean equals(Object obj) { throw new AssertionError();}
面試官:那麼你知道覆寫equals時有哪些準則?
這個我在Effective Java上看過,沒記錯的話應該是:
自反性:對於任何非空引用值 x,x.equals(x) 都應返回 true。
對稱性:對於任何非空引用值 x 和 y,當且僅當 y.equals(x) 返回 true 時,x.equals(y) 才應返回 true。
傳遞性:對於任何非空引用值 x、y 和 z,如果 x.equals(y) 返回 true, 並且 y.equals(z) 返回 true,那麼 x.equals(z) 應返回 true。
一致性:對於任何非空引用值 x 和 y,多次呼叫 x.equals(y) 始終返回 true 或始終返回 false, 前提是物件上 equals 比較中所用的資訊沒有被修改。
非空性:對於任何非空引用值 x,x.equals(null) 都應返回 false。
面試官:說說哪些情況下會違反對稱性和傳遞性
違反對稱性
對稱性就是x.equals(y)時,y也得equals x,很多時候,我們自己覆寫equals時,讓自己的類可以相容等於一個已知類,比如下面的例子:
public final class CaseInsensitiveString {
private final String s;
public CaseInsensitiveString(String s) {
if (s == null)
throw new NullPointerException();
this.s = s;
}
@Override
public boolean equals(Object o) {
if (o instanceof CaseInsensiticeString)
return s.equalsIgnoreCase(((CaseInsensitiveString)o).s);
if (o instanceof String)
return s.equalsIgnoreCase((String) o);
return false;
}
}
複製程式碼
這個想法很好,想建立一個無視大小寫的String,並且還能夠相容String作為引數,假設我們建立一個CaseInsensitiveString:
CaseInsensitiveString cis = new CaseInsensitiveString("Case");
複製程式碼
那麼肯定有cis.equals("case")
,問題來了,"case".equals(cis)
嗎?String並沒有相容CaseInsensiticeString,所以String的equals也不接受CaseInsensiticeString作為引數。
所以有個準則,一般在覆寫equals只相容同型別的變數。
違反傳遞性
傳遞性就是A等於B,B等於C,那麼A也應該等於C。
假設我們定義一個類Cat。
public class Cat()
{
private int height;
private int weight;
public Cat(int h, int w)
{
this.height = h;
this.weight = w;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Cat))
return false;
Cat c = (Cat) o;
return c.height == height && c.weight == weight;
}
}
複製程式碼
名人有言,不管黑貓白貓抓住老鼠就是好貓,我們又定義一個類ColorCat:
public class ColorCat extends()
{
private String color;
public ColorCat(int h, int w, String color)
{
super(h, w);
this.color = color;
}
複製程式碼
我們在實現equals方法時,可以加上顏色比較,但是加上顏色就不相容和普通貓作對比了,這裡我們忘記上面要求只相容同型別變數的建議,定義一個相容普通貓的equals方法,在“混合比較”時忽略顏色。
@Override
public boolean equals(Object o) {
if (! (o instanceof Cat))
return false; //不是Cat或者ColorCat,直接false
if (! (o instanceof ColorCat))
return o.equals(this);//不是彩貓,那一定是普通貓,忽略顏色對比
return super.equals(o)&&((ColorCat)o).color.equals(color); //這時候才比較顏色
}
複製程式碼
假設我們定義了貓:
ColorCat whiteCat = new ColorCat(1,2,"white");
Cat cat = new Cat(1,2);
ColorCat blackCat = new ColorCat(1,2,"black");
複製程式碼
此時有whiteCat等於cat,cat等於blackCat,但是whiteCat不等於blackCat,所以不滿足傳遞性要求。。
所以在覆寫equals時,一定要遵守上述的5大軍規,不然總是有麻煩事找上門來。
面試官,那你在工作中有覆寫equals方法的訣竅嗎,比如寫一下String裡面的equals?
手寫:
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
複製程式碼
上面的equals有以下幾點訣竅:
- 使用==操作符檢查“引數是否為這個物件的引用”:如果是物件本身,則直接返回,攔截了對本身呼叫的情況,算是一種效能優化。
- 使用instanceof操作符檢查“引數是否是正確的型別”:如果不是,就返回false,正如對稱性和傳遞性舉例子中說得,不要想著相容別的型別,很容易出錯。在實踐中檢查的型別多半是equals所在類的型別,或者是該類實現的介面的型別,比如Set、List、Map這些集合介面。
- 把引數轉化為正確的型別: 經歷了上一步的檢測,基本會成功。
- 對於該類中的“關鍵域”,檢查引數中的域是否與物件中的對應域相等:基本型別的域就用
==
比較,float域用Float.compare方法,double域用Double.compare方法,至於別的引用域,我們一般遞迴呼叫它們的equals方法比較,加上判空檢查和對自身引用的檢查,一般會寫成這樣:(field == o.field || (field != null && field.equals(o.field)))
,而上面的String裡使用的是陣列,所以只要把陣列中的每一位拿出來比較就可以了。 - 編寫完成後思考是否滿足上面提到的對稱性,傳遞性,一致性等等。
還有一些注意點。
覆蓋equals時一定要覆蓋hashCode
equals函式裡面一定要是Object型別作為引數
equals方法本身不要過於智慧,只要判斷一些值相等即可。
面試官,剛才提到了hashCode,有什麼用?
hashCode用於返回物件的hash值,主要用於查詢的快捷性,因為hashCode也是在Object物件中就有的,所以所有Java物件都有hashCode,在HashTable和HashMap這一類的雜湊結構中,都是通過hashCode來查詢在雜湊表中的位置的。
如果兩個物件equals,那麼它們的hashCode必然相等,
但是hashCode相等,equals不一定相等。
以HashMap為例,使用的是鏈地址法來處理雜湊,假設有一個長度為8的雜湊表
0 1 2 3 4 5 6 7
複製程式碼
那麼,當往裡面插資料時,是以hashCode作為key插入的,一般hashCode%8得到所在的索引,如果所在索引處有元素了,則使用一個連結串列,把多的元素不斷連結到該位置,這邊也就是大概提一下HashMap原理。所以hashCode的作用就是找到索引的位置,然後再用equals去比較元素是不是相等,形象一點就是先找到桶(bucket),然後再在裡面找東西。
面試官:你知道有哪些覆寫hashCode的訣竅?
一個好的hashCode的方法的目標:為不相等的物件產生不相等的雜湊碼,同樣的,相等的物件必須擁有相等的雜湊碼。
好的雜湊函式要把例項均勻的分佈到所有雜湊值上,結合前人的經驗可以採取以下方法:
引自Effective Java
-
把某個非零的常數值,比如17,儲存在一個int型的result中;
-
對於每個關鍵域f(equals方法中設計到的每個域),作以下操作:
a. 為該域計算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. 如果是一個陣列,則把每一個元素當成一個單獨的域來處理。 複製程式碼
b.result = 31 * result + c;
-
返回result
-
編寫單元測試驗證有沒有實現所有相等的例項都有相等的雜湊碼。
這裡再說下2.b中為什麼採用31*result + c
,乘法使hash值依賴於域的順序,如果沒有乘法那麼所有順序不同的字串String物件都會有一樣的hash值,而31是一個奇素數,如果是偶數,並且乘法溢位的話,資訊會丟失,31有個很好的特性是31*i ==(i<<5)-i
,即2的5次方減1,虛擬機器會優化乘法操作為移位操作的。