equals和hashCode的區(qū)別和聯(lián)系

一、前言

前段時(shí)間使用list.remove(obj)的時(shí)候重寫了obj的equals方法,因?yàn)閘ist的remove是以equals來判斷標(biāo)準(zhǔn)的。但是,今天被公司的代碼掃描工具提示未重寫hashCode方法??!之前準(zhǔn)備面試時(shí)也多少看過,但是沒有細(xì)細(xì)研究過這個(gè)hashCode和equals到底背后是什么個(gè)關(guān)系,趁此機(jī)會(huì),總結(jié)一波。

本文章所用到的自定義測試對(duì)象類Stu:

public class Stu {
  private String name;
  private int age;

  Stu(String name, int age) {
    this.name = name;
    this.age = age;
  }

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public int getAge() {
    return age;
  }

  public void setAge(int age) {
    this.age = age;
  }
}

二、equals的具體作用

首先要說的是equals是Object的方法,所以只能用于對(duì)象間,基本類型之間比較用“==”,反則他們的封裝類型可以用equals。

public static void main(String[] args) {
  Stu s1 = new Stu("張三", 18);
  Stu s2 = new Stu("張三", 18);
  System.out.println("stu:" + s1.equals(s2));

  Integer i1 = new Integer(18);
  Integer i2 = new Integer(18);
  System.out.println("Integer:" + i1.equals(i2));

  String str1 = "張三";
  String str2 = "張三";
  System.out.println("String:" + str1.equals(str2));
}

很簡單,可以得到下面的結(jié)果:

stu:false
Integer:true
String:true

通過idea工具可以看到各自的equals實(shí)現(xiàn)代碼:

Stu

public boolean equals(Object obj) {
    return (this == obj);
}

Integer

public boolean equals(Object obj) {
    if (obj instanceof Integer) {
        return value == ((Integer)obj).intValue();
    }
    return false;
}

String

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;
}

Stu因?yàn)闆]有重寫equals方法,所以直接使用的父類Object的equals方法,后面Integer和String都各自實(shí)現(xiàn)了自己的equals方法,所以Integer(基本類型)的equals實(shí)際上都是用的自己的實(shí)際值比較,String則是逐個(gè)char比較相等于否。

三、hashCode的具體作用

hashcode方法返回該對(duì)象的哈希碼值。支持該方法是為哈希表提供一些優(yōu)點(diǎn),例如,java.util.Hashtable 提供的哈希表。

hashCode 的常規(guī)協(xié)定是:
在 Java 應(yīng)用程序執(zhí)行期間,在同一對(duì)象上多次調(diào)用 hashCode 方法時(shí),必須一致地返回相同的整數(shù),前提是對(duì)象上 equals 比較中所用的信息沒有被修改。從某一應(yīng)用程序的一次執(zhí)行到同一應(yīng)用程序的另一次執(zhí)行,該整數(shù)無需保持一致。

以下情況不 是必需的:如果根據(jù) equals(java.lang.Object) 方法,兩個(gè)對(duì)象不相等,那么在兩個(gè)對(duì)象中的任一對(duì)象上調(diào)用 hashCode 方法必定會(huì)生成不同的整數(shù)結(jié)果。但是,程序員應(yīng)該知道,為不相等的對(duì)象生成不同整數(shù)結(jié)果可以提高哈希表的性能。

實(shí)際上,由 Object 類定義的 hashCode 方法確實(shí)會(huì)針對(duì)不同的對(duì)象返回不同的整數(shù)。(這一般是通過將該對(duì)象的內(nèi)部地址轉(zhuǎn)換成一個(gè)整數(shù)來實(shí)現(xiàn)的,但是 JavaTM 編程語言不需要這種實(shí)現(xiàn)技巧。)

當(dāng)equals方法被重寫時(shí),通常有必要重寫 hashCode 方法,以維護(hù) hashCode 方法的常規(guī)協(xié)定,該協(xié)定聲明相等對(duì)象必須具有相等的哈希碼。

上面是引用的官方文檔上面的一段話,我們需要他說人話:

  1. 對(duì)象equals方法參與運(yùn)算的自身屬性attr不能被修改,并且同一個(gè)對(duì)象的hashCode值任何時(shí)候的返回值都應(yīng)該相等;
  2. hashCode不等的兩個(gè)對(duì)象equals一定不相等,但是hashCode相等的兩個(gè)對(duì)象equals不一定相等;
  3. 根據(jù)規(guī)定,重寫對(duì)象的equals方法必須重寫hashCode方法,盡管不寫也能通過編譯;

這里引用網(wǎng)上一個(gè)很容易理解的例子:

hashcode是用來查找的,如果你學(xué)過數(shù)據(jù)結(jié)構(gòu)就應(yīng)該知道,在查找和排序這一章有
例如內(nèi)存中有這樣的位置
0 1 2 3 4 5 6 7
而我有個(gè)類,這個(gè)類有個(gè)字段叫id,我要把這個(gè)類存放在以上8個(gè)位置之一,如果不用hashcode而任意存放,那么當(dāng)查找時(shí)就需要到這八個(gè)位置里挨個(gè)去找,或者用二分法一類的算法。
但如果用hashCode那就會(huì)使效率提高很多。
我們這個(gè)類中有個(gè)字段叫id,那么我們就定義我們的hashCode為id%8,然后把我們的類存放在取得得余數(shù)那個(gè)位置。比如我們的ID為9,9除8的余數(shù)為1,那么我們就把該類存在1這個(gè)位置,如果ID是13,求得的余數(shù)是5,那么我們就把該類放在5這個(gè)位置。這樣,以后在查找該類時(shí)就可以通過ID除 8求余數(shù)直接找到存放的位置了。

但是如果兩個(gè)類有相同的hashCode怎么辦那(我們假設(shè)上面的類的id不是唯一的),例如9除以8和17除以8的余數(shù)都是1,那么這是不是合法的,回答是:完全合法。那么如何判斷呢?在這個(gè)時(shí)候就需要定義equals了。
也就是說,我們先通過 hashCode來判斷兩個(gè)類是否存放某個(gè)桶里,但這個(gè)桶里可能有很多類,那么我們就需要再通過 equals 來在這個(gè)桶里找到我們要的類。
那么。重寫了equals(),為什么還要重寫hashCode()呢?
想想,你要在一個(gè)桶里找東西,你必須先要找到這個(gè)桶啊,你不通過重寫hashCode()來找到桶,光重寫equals()有什么用啊。

可能太過文本的東西沒有什么說服力,那就來點(diǎn)干貨:

public static void main(String[] args) {
  Stu s1 = new Stu("張三", 18);
  Stu s2 = new Stu("張三", 18);
  System.out.println("stu:" + s1.equals(s2));

  Set<Stu> set = new HashSet<>();
  set.add(s1);
  System.out.println("s1 hashCode:" + s1.hashCode());
  System.out.println("add s1 size:" + set.size());
  set.add(s2);
  System.out.println("s2 hashCode:" + s2.hashCode());
  System.out.println("add s2 size::" + set.size());
}

輸出結(jié)果:

stu:false
s1 hashCode:1317241155
add s1 size:1
s2 hashCode:463175162
add s2 size::2

Java中的Set是不允許有重復(fù)元素的,所以這里set的size由1變成了2,因?yàn)閮蓚€(gè)Stu都是new出來的,分配的地址不一樣,那么Set是通過equals來定義重復(fù)的嗎?

首先重寫Stu的equals方法:

@Override
public boolean equals(Object obj) {
  if (obj == null){
    return false;
  }
  if (obj.getClass() != getClass()){
    return false;
  }
  return ((Stu)obj).getName().equals(getName());
}

輸出結(jié)果:

stu:true
s1 hashCode:713679046
add s1 size:1
s2 hashCode:1107557627
add s2 size::2

重寫equals方法,name相同就讓equals返回true了,但是Set的size還是發(fā)生了改變,就說明不是有equals方法來定義重復(fù)的,現(xiàn)在僅僅重寫hashCode方法:

@Override
public int hashCode() {
  return getName().hashCode();
}

輸出結(jié)果:

stu:false
s1 hashCode:774889
add s1 size:1
s2 hashCode:774889
add s2 size::2

僅重寫了hashCode方法,所以equals返回false,然后hashCode由name屬性的hashCode方法得到,所以hashCode相等,但是Set的size還是改變了,這說明Set也不是僅僅依據(jù)hashCode來定義重復(fù)。

那么現(xiàn)在將上述equals和hashCode兩者同時(shí)重寫,輸出結(jié)果:

stu:true
s1 hashCode:774889
add s1 size:1
s2 hashCode:774889
add s2 size::1

結(jié)合上面引用的案例,可以類推,hash類存儲(chǔ)結(jié)構(gòu)(HashSet、HashMap等等)添加元素會(huì)有重復(fù)性校驗(yàn),校驗(yàn)的方式就是先取hashCode判斷是否相等(找到對(duì)應(yīng)的位置,該位置可能存在多個(gè)元素),然后再取equals方法比較(極大縮小比較范圍,高效判斷),最終判定該存儲(chǔ)結(jié)構(gòu)中是否有重復(fù)元素。

四、總結(jié)

  1. hashCode主要用于提升查詢效率,來確定在散列結(jié)構(gòu)中對(duì)象的存儲(chǔ)地址;
  2. 重寫equals()必須重寫hashCode(),二者參與計(jì)算的自身屬性字段應(yīng)該相同;
  3. hash類型的存儲(chǔ)結(jié)構(gòu),添加元素重復(fù)性校驗(yàn)的標(biāo)準(zhǔn)就是先取hashCode值,后判斷equals();
  4. equals()相等的兩個(gè)對(duì)象,hashcode()一定相等;
  5. 反過來:hashcode()不等,一定能推出equals()也不等;
  6. hashcode()相等,equals()可能相等,也可能不等。

五、花邊:通用的hashCode重寫方案

初始化一個(gè)整形變量,為此變量賦予一個(gè)非零的常數(shù)值,比如int result = 17;
選取equals方法中用于比較的所有域,然后針對(duì)每個(gè)域的屬性進(jìn)行計(jì)算:

  1. 如果是boolean值,則計(jì)算f ? 1:0
  2. 如果是byte\char\short\int,則計(jì)算(int)f
  3. 如果是long值,則計(jì)算(int)(f ^ (f >>> 32))
  4. 如果是float值,則計(jì)算Float.floatToIntBits(f)
  5. 如果是double值,則計(jì)算Double.doubleToLongBits(f),然后返回的結(jié)果是long,再用規(guī)則(3)去處理long,得到int
  6. 如果是對(duì)象應(yīng)用,如果equals方法中采取遞歸調(diào)用的比較方式,那么hashCode中同樣采取遞歸調(diào)用hashCode的方式。否則需要為這個(gè)域計(jì)算一個(gè)范式,比如當(dāng)這個(gè)域的值為null的時(shí)候,那么hashCode 值為0
  7. 如果是數(shù)組,那么需要為每個(gè)元素當(dāng)做單獨(dú)的域來處理。如果你使用的是1.5及以上版本的JDK,那么沒必要自己去重新遍歷一遍數(shù)組,java.util.Arrays.hashCode方法包含了8種基本類型數(shù)組和引用數(shù)組的hashCode計(jì)算,算法同上

給個(gè)簡單的例子:

@Override
public int hashCode() {
  int result = 17;
  result = 31 * result + getName().hashCode();
  return result;
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容