面試官愛問的equals與hashCode

卡巴拉的樹發表於2017-12-28

equals和hashCode都是Object物件中的非final方法,它們設計的目的就是被用來覆蓋(override)的,所以在程式設計中還是經常需要處理這兩個方法的。而掌握這兩個方法的覆蓋準則以及它們的區別還是很必要的,相關問題也不少。

面試官愛問的equals與hashCode

下面我們繼續以一次面試的問答,來考察對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

  1. 把某個非零的常數值,比如17,儲存在一個int型的result中;

  2. 對於每個關鍵域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;

  3. 返回result

  4. 編寫單元測試驗證有沒有實現所有相等的例項都有相等的雜湊碼。

這裡再說下2.b中為什麼採用31*result + c,乘法使hash值依賴於域的順序,如果沒有乘法那麼所有順序不同的字串String物件都會有一樣的hash值,而31是一個奇素數,如果是偶數,並且乘法溢位的話,資訊會丟失,31有個很好的特性是31*i ==(i<<5)-i,即2的5次方減1,虛擬機器會優化乘法操作為移位操作的。

相關文章