面試官愛問的equals與hashCode

equals和hashCode都是Object對象中的非final方法,它們設計的目的就是被用來覆蓋(override)的,因此在程序設計中仍是常常須要處理這兩個方法的。而掌握這兩個方法的覆蓋準則以及它們的區別仍是很必要的,相關問題也很多。面試

下面咱們繼續以一次面試的問答,來考察對equals和hashCode的掌握狀況。數組

面試官: Java裏面有==運算符了,爲何還須要equals啊?


equals()的做用是用來判斷兩個對象是否相等,在Object裏面的定義是:性能優化

public boolean equals(Object obj) {
    return (this == obj);
}
複製代碼

這說明在咱們實現本身的equals方法以前,equals等價於==,而==運算符是判斷兩個對象是否是同一個對象,即他們的地址是否相等。而覆寫equals更多的是追求兩個對象在邏輯上的相等,你能夠說是值相等,也可說是內容相等bash

在如下幾種條件中,不覆寫equals就能達到目的:dom

  • 類的每一個實例本質上是惟一的:強調活動實體的而不關心值得,好比Thread,咱們在意的是哪個線程,這時候用equals就能夠比較了。
  • 不關心類是否提供了邏輯相等的測試功能:有的類的使用者不會用到它的比較值得功能,好比Random類,基本沒人會去比較兩個隨機值吧
  • 超類已經覆蓋了equals,子類也只須要用到超類的行爲:好比AbstractMap裏已經覆寫了equals,那麼繼承的子類行爲上也就須要這個功能,那也不須要再實現了。
  • 類是私有的或者包級私有的,那也用不到equals方法:這時候須要覆寫equals方法來禁用它:@Override public boolean equals(Object obj) { throw new AssertionError();}

面試官:那麼你知道覆寫equals時有哪些準則?


這個我在Effective Java上看過,沒記錯的話應該是:ide

自反性:對於任何非空引用值 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,虛擬機會優化乘法操做爲移位操做的。

相關文章
相關標籤/搜索