覆蓋equals時(shí)總要覆蓋hashCode

1. 什么是hashcode方法?
  • hashcode方法返回對(duì)象的哈希碼值
  • 在應(yīng)用程序的執(zhí)行期間,只要對(duì)象的equals方法的比較操作所用到的信息沒(méi)有改變,那么對(duì)于這同一個(gè)對(duì)象調(diào)用多次,hashcode方法都必須返回同一個(gè)整數(shù)。
  • hashcode的存在主要用于查找的快捷性,如Hashtable,HashMap等,hashcode是用來(lái)在散列存儲(chǔ)結(jié)構(gòu)中確定對(duì)象的存儲(chǔ)地址的。
2. hashcode相等與對(duì)象相等之間的關(guān)系:(保證設(shè)計(jì)是規(guī)范的前提下)
  • 如果兩個(gè)對(duì)象相同,那么兩個(gè)對(duì)象的hashcode也必須相同
  • 如果兩個(gè)對(duì)象的hashcode相同,并不一定表示兩個(gè)對(duì)象就相同,也就是不一定適合equals方法,只能夠說(shuō)明兩個(gè)對(duì)象在散列表存儲(chǔ)結(jié)構(gòu)中,“存放在同一個(gè)籃子里”。
3. 為什么覆蓋equals方法時(shí)總要覆蓋hashcode方法?

因?yàn)槿绻桓采wequals方法的話,相等的對(duì)象可能返回的不相同的hash code。

例子:我們創(chuàng)建兩個(gè)相同值的對(duì)象,然后將p1對(duì)象作為key,"jenny"插入到hashmap中,再將p2對(duì)象作為key,獲取對(duì)應(yīng)的value值

public class PhoneNumber {
    private final short areaCode;
    private final short prefix;
    private final short lineNumber;

    public PhoneNumber(int areaCode, int prefix, int lineNumber) {
        this.areaCode = (short) areaCode;
        this.prefix = (short) prefix;
        this.lineNumber = (short) lineNumber;
    }
    //覆蓋equals方法
    @Override
    public boolean equals(Object obj) {
        if (obj == this)
            return true;
        if (!(obj instanceof PhoneNumber))
            return false;
        //必須滿足如下條件,才能說(shuō)明為同一個(gè)對(duì)象
        PhoneNumber pn = (PhoneNumber) obj;
        return pn.areaCode == areaCode && pn.prefix == prefix && pn.lineNumber == lineNumber;
    }

    public static void main(String[] args){
        Map<PhoneNumber, String> m = new HashMap<>();
        //創(chuàng)建兩個(gè)相同的對(duì)象
        PhoneNumber p1 = new PhoneNumber(707, 867, 5309);
        PhoneNumber p2 = new PhoneNumber(707, 867, 5309);
        //添加到hashmap中
        m.put(p1, "Jenny");
        //比較對(duì)象p1和p2
        System.out.println("p1.equals(p2): " + p1.equals(p2));
        System.out.println("p2.equals(p1): " + p2.equals(p1));
        //從hashmap中去獲取對(duì)象p1和p2
        System.out.println("get p1 from hashmap: " + m.get(p1));
        System.out.println("get p2 from hashmap: " + m.get(p2));
    }
}
輸出結(jié)果:
p1.equals(p2): true
p2.equals(p1): true
get p1 from hashmap: Jenny
get p2 from hashmap: null

前兩個(gè)結(jié)果說(shuō)明p1和p2為同一對(duì)象,但我們發(fā)現(xiàn)以p1作為key時(shí),是可以獲取到對(duì)應(yīng)的value,而以p2作為key時(shí),卻獲取到null。

實(shí)際上,是因?yàn)镻honeNumber類沒(méi)有覆蓋hashcode方法,從而導(dǎo)致兩個(gè)相等的實(shí)例具有不相等的散列碼。

//輸出p1和p2的hashcode
System.out.println("p1's hashcode: " + p1.hashCode());
System.out.println("p2's hashcode: " + p2.hashCode());
輸出結(jié)果:
p1's hashcode: 460141958
p2's hashcode: 1163157884

因此,導(dǎo)致put方法時(shí)把電話存放在一個(gè)散列桶中,而get方法卻在另個(gè)散列桶中查找這個(gè)電話號(hào)碼。即使這兩個(gè)實(shí)例正好放到同一個(gè)散列桶中(發(fā)生“沖突”的情況),get方法也必定會(huì)返回null,因?yàn)閔ashmap有一項(xiàng)優(yōu)化,可以將與每個(gè)相關(guān)聯(lián)的散列碼緩存起來(lái),如果散列碼不匹配,那么將會(huì)返回null。

4. 如何在覆蓋equals方法時(shí)覆蓋hashcode方法?

實(shí)際上,問(wèn)題很簡(jiǎn)單,只要我們重寫hashcode方法,返回一個(gè)適當(dāng)?shù)膆ash code即可。

@Override
public int hashCode() {
    return 42;
}

這樣的確能解決上面的問(wèn)題,但實(shí)際上,這么做,會(huì)導(dǎo)致很差的性能,因?yàn)樗偸谴_保每個(gè)對(duì)象都具有同樣的散列碼。因此,每個(gè)對(duì)象都被映射到同一個(gè)散列桶中,使得散列表退化成鏈表。

** 一個(gè)好的散列函數(shù)通常傾向于“為不相等的對(duì)象產(chǎn)生不同的散列碼”**。
理想情況下,散列函數(shù)應(yīng)該把集合中不相等的實(shí)例均勻地分布到所有可能的散列值上。但實(shí)際上,要達(dá)到這種理想的情形是非常困難的。

5. 如何設(shè)置一個(gè)好的散列函數(shù)?

a. 為對(duì)象計(jì)算int類型的散列碼c:

  • 對(duì)于boolean類型,計(jì)算(f?1:0)
  • 對(duì)于byte,char,short,int類型,則計(jì)算(int)f
  • 對(duì)于long類型,計(jì)算(int)(f^(f>>>32))
  • 對(duì)于float類型,計(jì)算Float.floatToIntBits(f)
  • 對(duì)于Double類型,計(jì)算Double.doubleToLongBits(f),然后再按照l(shuí)ong類型處理
  • 對(duì)于對(duì)象引用,并且該類的equals方法通過(guò)遞歸地調(diào)用equals的方式來(lái)比較這個(gè)域,則同樣為這個(gè)域遞歸地調(diào)用hashcode
  • 對(duì)于數(shù)組,則把每一個(gè)元素當(dāng)作單獨(dú)的域來(lái)處理。

b. 將獲取到的c合并:result = 31 * reuslt + c;

c. 返回result

比如,我們可以優(yōu)化上面的hashcode方法:

    @Override
    public int hashCode() {
        int result = 17;
        result = 31 * result + areaCode;
        result = 31 * result + prefix;
        result = 31 * result + lineNumber;
        return result;
    }

如果一個(gè)類是不可變類,并且計(jì)算散列碼的開(kāi)銷也比較大,就應(yīng)該考慮吧散列碼緩存在對(duì)象內(nèi)部,而不是每次請(qǐng)求的時(shí)候都重新計(jì)算散列碼。

 private volatile static int hashcode;
    
    @Override
    public int hashCode() {
        int result = hashcode;
        if (result == 0){
            result = 31 * result + areaCode;
            result = 31 * result + prefix;
            result = 31 * result + lineNumber;
            hashcode = result;
        }
        return result;
    }
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容