equals和hashCode的區別和聯繫

equals和hashCode的區別和聯繫

一、前言

前段時間使用list.remove(obj)的時候重寫了obj的equals方法,因爲list的remove是以equals來判斷標準的。但是,今天被公司的代碼掃描工具提示未重寫hashCode方法!!之前準備面試時也多少看過,但是沒有細細研究過這個hashCode和equals到底背後是什麼個關係,趁此機會,總結一波。

本文章所用到的自定義測試對象類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的方法,所以只能用於對象間,基本類型之間比較用“==”,反則他們的封裝類型可以用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)); }

很簡單,可以得到下面的結果:

stu:false Integer:true String:true

通過idea工具可以看到各自的equals實現代碼:

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因爲沒有重寫equals方法,所以直接使用的父類Object的equals方法,後面Integer和String都各自實現了自己的equals方法,所以Integer(基本類型)的equals實際上都是用的自己的實際值比較,String則是逐個char比較相等於否。

三、hashCode的具體作用

hashcode方法返回該對象的哈希碼值。支持該方法是爲哈希表提供一些優點,例如,java.util.Hashtable 提供的哈希表。

hashCode 的常規協定是:

在 Java 應用程序執行期間,在同一對象上多次調用 hashCode 方法時,必須一致地返回相同的整數,前提是對象上 equals 比較中所用的信息沒有被修改。從某一應用程序的一次執行到同一應用程序的另一次執行,該整數無需保持一致。

以下情況不 是必需的:如果根據 equals(java.lang.Object) 方法,兩個對象不相等,那麼在兩個對象中的任一對象上調用 hashCode 方法必定會生成不同的整數結果。但是,程序員應該知道,爲不相等的對象生成不同整數結果可以提高哈希表的性能。

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

當equals方法被重寫時,通常有必要重寫 hashCode 方法,以維護 hashCode 方法的常規協定,該協定聲明相等對象必須具有相等的哈希碼。

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

  1. 對象equals方法參與運算的自身屬性attr不能被修改,並且同一個對象的hashCode值任何時候的返回值都應該相等;
  2. hashCode不等的兩個對象equals一定不相等,但是hashCode相等的兩個對象equals不一定相等;
  3. 根據規定,重寫對象的equals方法必須重寫hashCode方法,儘管不寫也能通過編譯;

這裏引用網上一個很容易理解的例子:

hashcode是用來查找的,如果你學過數據結構就應該知道,在查找和排序這一章有

例如內存中有這樣的位置

0 1 2 3 4 5 6 7

而我有個類,這個類有個字段叫id,我要把這個類存放在以上8個位置之一,如果不用hashcode而任意存放,那麼當查找時就需要到這八個位置裏挨個去找,或者用二分法一類的算法。

但如果用hashCode那就會使效率提高很多。

我們這個類中有個字段叫id,那麼我們就定義我們的hashCode爲id%8,然後把我們的類存放在取得得餘數那個位置。比如我們的ID爲9,9除8的餘數爲1,那麼我們就把該類存在1這個位置,如果ID是13,求得的餘數是5,那麼我們就把該類放在5這個位置。這樣,以後在查找該類時就可以通過ID除 8求餘數直接找到存放的位置了。

但是如果兩個類有相同的hashCode怎麼辦那(我們假設上面的類的id不是唯一的),例如9除以8和17除以8的餘數都是1,那麼這是不是合法的,回答是:完全合法。那麼如何判斷呢?在這個時候就需要定義equals了。

也就是說,我們先通過 hashCode來判斷兩個類是否存放某個桶裏,但這個桶裏可能有很多類,那麼我們就需要再通過 equals 來在這個桶裏找到我們要的類。

那麼。重寫了equals(),爲什麼還要重寫hashCode()呢?

想想,你要在一個桶裏找東西,你必須先要找到這個桶啊,你不通過重寫hashCode()來找到桶,光重寫equals()有什麼用啊。

可能太過文本的東西沒有什麼說服力,那就來點乾貨:

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

輸出結果:

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

Java中的Set是不允許有重複元素的,所以這裏set的size由1變成了2,因爲兩個Stu都是new出來的,分配的地址不一樣,那麼Set是通過equals來定義重複的嗎?

首先重寫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()); }

輸出結果:

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

重寫equals方法,name相同就讓equals返回true了,但是Set的size還是發生了改變,就說明不是有equals方法來定義重複的,現在僅僅重寫hashCode方法:

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

輸出結果:

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也不是僅僅依據hashCode來定義重複。

那麼現在將上述equals和hashCode兩者同時重寫,輸出結果:

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

結合上面引用的案例,可以類推,hash類存儲結構(HashSet、HashMap等等)添加元素會有重複性校驗,校驗的方式就是先取hashCode判斷是否相等(找到對應的位置,該位置可能存在多個元素),然後再取equals方法比較(極大縮小比較範圍,高效判斷),最終判定該存儲結構中是否有重複元素。

四、總結

  1. hashCode主要用於提升查詢效率,來確定在散列結構中對象的存儲地址;
  2. 重寫equals()必須重寫hashCode(),二者參與計算的自身屬性字段應該相同;
  3. hash類型的存儲結構,添加元素重複性校驗的標準就是先取hashCode值,後判斷equals();
  4. equals()相等的兩個對象,hashcode()一定相等;
  5. 反過來:hashcode()不等,一定能推出equals()也不等;
  6. hashcode()相等,equals()可能相等,也可能不等。

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

初始化一個整形變量,爲此變量賦予一個非零的常數值,比如int result = 17;

選取equals方法中用於比較的所有域,然後針對每個域的屬性進行計算:

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

java.util.Arrays.hashCode方法包含了8種基本類型數組和引用數組的hashCode計算,算法同上,

  java.util.Arrays.hashCode(long[])的具體實現:

public static int hashCode(long a[]) {  

        if (a == null)  

            return 0;  

        int result = 1;  

        for (long element : a) {  

            int elementHash = (int)(element ^ (element >>> 32));  

            result = 31 * result + elementHash;  

        }  

        return result;  

}  

public static int hashCode(long a[]) {

if (a == null)

return 0;

int result = 1;

for (long element : a) {

int elementHash = (int)(element ^ (element >>> 32));

result = 31 * result + elementHash;

}

return result;

}

 

Arrays.hashCode(...)只會計算一維數組元素的hashCOde,如果是多維數組,那麼需要遞歸進行hashCode的計算,那麼就需要使用Arrays.deepHashCode(Object[])方法。

3. 最後,要如同上面的代碼,把每個域的散列碼合併到result當中:result = 31 * result + elementHash;

4. 測試,hashCode方法是否符合文章開頭說的基本原則,這些基本原則雖然不能保證性能,但是可以保證不出錯。

2. 爲什麼每次需要使用乘法去操作result? 主要是爲了使散列值依賴於域的順序,還是上面的那個例子,Test t = new Test(1, 0)跟Test t2 = new Test(0, 1), t和t2的最終hashCode返回值是不一樣的。

3. 爲什麼是31? 31是個神奇的數字,因爲任何數n * 31就可以被JVM優化爲 (n << 5) -n,移位和減法的操作效率要比乘法的操作效率高的多。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章