1. 程式人生 > >牛客網Java刷題知識點之equals和hashcode()

牛客網Java刷題知識點之equals和hashcode()

不多說,直接上乾貨!

  說簡單點,就是,先對hashcode,然後對eauals。

  以下是HashMap的jdk1.6   : 

  以下是HashMap的jdk1.7  : 

  以下是HashMap的jdk1.8  : 

equal() 和hashCode()方法都是基類Object的方法

  其原始碼如下:

複製程式碼

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

//hashCode method
/*Returns a hash code value for the object. This method is
  supported for the benefit of hash tables such as those provided by {@link java.util.HashMap}.*/

public native int hashCode();

複製程式碼

  因此,equal()方法就是直接比較物件的地址,對於hashCode(),Java採用了雜湊表的原理。 雜湊演算法也稱為雜湊演算法,是將資料依特定演算法直接指定到一個地址上。初學者可以這樣理解,hashCode方法實際上返回的就是物件儲存的實體地址(實際可能並不是)。

那麼問題來了? 為什麼重寫equal()方法的時候通常需要重寫hashCode()方法呢?

  equal()方法是比較物件是否相等的,hashCode()方法是在當物件存入集合如HashSet物件,以及HashMap物件、HashTable等裡面用的,這樣一來,當集合要新增新的元素時,先呼叫這個元素的hashCode方法,就一下子能定位到它應該放置的物理位置上。 如果這個位置上沒有元素,它就可以直接儲存在這個位置上,不用再進行任何比較了;如果這個位置上已經有元素了, 就呼叫它的equals方法與新元素進行比較,相同的話就覆蓋,不相同就雜湊其它的地址。 所以這裡存在一個衝突解決的問題。這樣一來實際呼叫equals方法的次數就大大降低了,幾乎只需要一兩次

  比如String方法裡重寫了equal()方法:

複製程式碼

//String重寫的equal方法
  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;
    }


//hashCode()方法
 public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

複製程式碼

  例子:

複製程式碼

package com.demo;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

public class Equal {

    public static void main(String[] args) {
        Map<String,String> maps = new HashMap<String,String>();
        maps.put("1", "zhangsan");
        maps.put("1", "lisi");
        maps.put("2", "wangwu");
        maps.put("2", "zhaoliu");
        maps.put("3", "zhaoliu");

        Iterator<Map.Entry<String, String>> iterator = maps.entrySet().iterator();
        while(iterator.hasNext()){
            Map.Entry<String, String> entry = iterator.next();
            System.out.println(entry.getKey()+entry.getValue());
        }
    }
}
//輸出結果:
// 1lisi
// 2zhaoliu
// 3zhaoliu

複製程式碼

  總之    1、equal()是判斷兩個物件是否相同的    2、hashCode()是在HashSet、HashMap等中用的    3. 通常equal() 和hashCode()要保證物件的一致性。。。。

  以下是關於HashCode的官方文件定義:

  hashcode方法返回該物件的雜湊碼值。支援該方法是為雜湊表提供一些優點,例如,java.util.Hashtable 提供的雜湊表。    在 Java 應用程式執行期間,在同一物件上多次呼叫 hashCode 方法時,必須一致地返回相同的整數,前提是物件上 equals 比較中所用的資訊沒有被修改。從某一應用程式的一次執行到同一應用程式的另一次執行,該整數無需保持一致。    如果根據 equals(Object) 方法,兩個物件是相等的,那麼在兩個物件中的每個物件上呼叫 hashCode 方法都必須生成相同的整數結果。    以下情況不 是必需的:如果根據 equals(java.lang.Object) 方法,兩個物件不相等,那麼在兩個物件中的任一物件上呼叫 hashCode 方法必定會生成不同的整數結果。但是,程式設計師應該知道,為不相等的物件生成不同整數結果可以提高雜湊表的效能。    實際上,由 Object 類定義的 hashCode 方法確實會針對不同的物件返回不同的整數。(這一般是通過將該物件的內部地址轉換成一個整數來實現的,但是 JavaTM 程式語言不需要這種實現技巧。)    當equals方法被重寫時,通常有必要重寫 hashCode 方法,以維護 hashCode 方法的常規協定,該協定宣告相等物件必須具有相等的雜湊碼。

 以上這段官方文件的定義,我們可以抽出成以下幾個關鍵點:

  1、hashCode的存在主要是用於查詢的快捷性,如Hashtable,HashMap等,hashCode是用來在雜湊儲存結構中確定物件的儲存地址的;

  2、如果兩個物件相同,就是適用於equals(java.lang.Object) 方法,那麼這兩個物件的hashCode一定要相同;

  3、如果物件的equals方法被重寫,那麼物件的hashCode也儘量重寫,並且產生hashCode使用的物件,一定要和equals方法中使用的一致,否則就會違反上面提到的第2點;

  4、兩個物件的hashCode相同,並不一定表示兩個物件就相同,也就是不一定適用於equals(java.lang.Object) 方法,只能夠說明這兩個物件在雜湊儲存結構中,如Hashtable,他們“存放在同一個籃子裡”

再歸納一下

  就是hashCode是用於查詢使用的,而equals是用於比較兩個物件的是否相等的。

1、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求餘數直接找到存放的位置了。  2、但是如果兩個類有相同的hashcode怎麼辦那(我們假設上面的類的ID不是唯一的),例如9除以8和17除以8的餘數都是1,那麼這是不是合法的,回答是:可以這樣。那麼如何判斷呢?在這個時候就需要定義 equals了。    也就是說,我們先通過 hashcode來判斷兩個類是否存放某個桶裡,但這個桶裡可能有很多類,那麼我們就需要再通過 equals 來在這個桶裡找到我們要的類。    那麼。重寫了equals(),為什麼還要重寫hashCode()呢?    想想,你要在一個桶裡找東西,你必須先要找到這個桶啊,你不通過重寫hashcode()來找到桶,光重寫equals()有什麼用啊。

從HashMap中通過key查詢value時的過程?

  首先呼叫的是key的hashcode()方法來獲取到key對應的hash值h,這樣就可以確定鍵為key的所有值儲存的首地址。

  如果h對應的key值有多個,那麼程式接著會遍歷所有的key,通過呼叫key的equals()方法來判斷key的內容是否相等。只有當equals()方法的返回值為true時,對應的value才是正確的結果。

  比如:如首先向HashMap中新增<“aaa”,"bbb">,接著新增<“aaa”,"ccc">的時候由於與前面新增的資料有相同的key:"aaa",因此會用新的值"ccc"替換“bbb”。

對於不同的key值可能會得到相同的hash值,因此就需要對衝突進行處理

  一般而言,對於不同的key值可能會得到相同的hash值,因此就需要對衝突進行處理。一般處理衝突的方法是:開放地址法、再hash法、鏈地址法等。

HashMap新增元素的操作過程

  在向HashMap中新增鍵值對<key,value>時,需要經過以下幾個步驟:

  首先,呼叫key的hashcode()方法生成一個hash值h1,如果這個h1在HashMap中不存在,那麼直接將<key,value>新增到HashMap中,如果這個h1已經存在,那麼找出HashMap中所有hash值為h1的key。

  然後,分別呼叫key的equals()方法判斷當前新增的key值是否與已經存在的key值相同。如果equals()方法返回true,說明當前需要新增的key已經存在,那麼HashMap會使用新的value值來覆蓋舊的value值。如果equals()方法返回false,說明新增加的key在HashMap中不存在,因此會在HashMap中建立新的對映關係。當新增加的key的hash值已經在HashMap中存在時,就會產生衝突。

  最近去面試了幾家公司,被問到hashCode的作用,雖然回答出來了,但是自己還是對hashCode和equals的作用一知半解的,所以決定把它們研究一下。

以前寫程式一直沒有注意hashCode的作用,一般都是覆蓋了equals,卻沒有覆蓋hashCode,現在發現這是埋下了很多潛在的Bug!今天就來說一說hashCode和equals的作用。

       先來試想一個場景,如果你想查詢一個集合中是否包含某個物件,那麼程式應該怎麼寫呢?通常的做法是逐一取出每個元素與要查詢的物件一一比較,當發現兩者進行equals比較結果相等時,則停止查詢並返回true,否則,返回false。但是這個做法的一個缺點是當集合中的元素很多時,譬如有一萬個元素,那麼逐一的比較效率勢必下降很快。於是有人發明了一種雜湊演算法來提高從該集合中查詢元素的效率,這種方式將集合分成若干個儲存區域(可以看成一個個桶),每個物件可以計算出一個雜湊碼,可以根據雜湊碼分組,每組分別對應某個儲存區域,這樣一個物件根據它的雜湊碼就可以分到不同的儲存區域(不同的桶中)。如下圖所示:

 

  實際的使用中,一個物件一般有key和value,可以根據key來計算它的hashCode。假設現在全部的物件都已經根據自己的hashCode值儲存在不同的儲存區域中了,那麼現在查詢某個物件(根據物件的key來查詢),不需要遍歷整個集合了,現在只需要計算要查詢物件的key的hashCode,然後找到該hashCode對應的儲存區域,在該儲存區域中來查詢就可以了,這樣效率也就提升了很多。說了這麼多相信你對hashCode的作用有了一定的瞭解。

下面就來看看hashCode和equals的區別和聯絡。

  在研究這個問題之前,首先說明一下JDK對equals(Object obj)和hashCode()這兩個方法的定義和規範:在Java中任何一個物件都具備equals(Object obj)和hashCode()這兩個方法,因為他們是在Object類中定義的。 equals(Object obj)方法用來判斷兩個物件是否“相同”,如果“相同”則返回true,否則返回false。 hashCode()方法返回一個int數,在Object類中的預設實現是“將該物件的內部地址轉換成一個整數返回”。

 下面是我查閱了相關資料之後對以上的說明做的歸納總結:

1、若重寫了equals(Object obj)方法,則有必要重寫hashCode()方法。 2、若兩個物件equals(Object obj)返回true,則hashCode()有必要也返回相同的int數。 3、若兩個物件equals(Object obj)返回false,則hashCode()不一定返回不同的int數。 4、若兩個物件hashCode()返回相同int數,則equals(Object obj)不一定返回true。 5、若兩個物件hashCode()返回不同int數,則equals(Object obj)一定返回false。 6、同一物件在執行期間若已經儲存在集合中,則不能修改影響hashCode值的相關資訊,否則會導致記憶體洩露問題。

  想要弄清楚以上六點,先要知道什麼時候需要重寫equals和hashCode。一般來說涉及到物件之間的比較大小就需要重寫equals方法,但是為什麼第一點說重寫了equals就需要重寫hashCode呢?實際上這只是一條規範,如果不這樣做程式也可以執行,只不過會隱藏bug。一般一個類的物件如果會儲存在HashTable,HashSet,HashMap等雜湊儲存結構中,那麼重寫equals後最好也重寫hashCode,否則會導致儲存資料的不唯一性(儲存了兩個equals相等的資料)。而如果確定不會儲存在這些雜湊結構中,則可以不重寫hashCode。但是個人覺得還是重寫比較好一點,誰能保證後期不會儲存在這些結構中呢,況且重寫了hashCode也不會降低效能,因為線上性結構(如ArrayList)中是不會呼叫hashCode,所以重寫了也不要緊,也為後期的修改打了補丁。

想要弄清楚以上六點,先要知道什麼時候需要重寫equals和hashCode。

  一般來說涉及到物件之間的比較大小就需要重寫equals方法,但是為什麼第一點說重寫了equals就需要重寫hashCode呢?實際上這只是一條規範,如果不這樣做程式也可以執行,只不過會隱藏bug。一般一個類的物件如果會儲存在HashTable,HashSet,HashMap等雜湊儲存結構中,那麼重寫equals後最好也重寫hashCode,否則會導致儲存資料的不唯一性(儲存了兩個equals相等的資料)。而如果確定不會儲存在這些雜湊結構中,則可以不重寫hashCode。但是個人覺得還是重寫比較好一點,誰能保證後期不會儲存在這些結構中呢,況且重寫了hashCode也不會降低效能,因為線上性結構(如ArrayList)中是不會呼叫hashCode,所以重寫了也不要緊,也為後期的修改打了補丁。

下面來看一張物件放入雜湊集合的流程圖:

  從上面的圖中可以清晰地看到在儲存一個物件時,先進行hashCode值的比較,然後進行equals的比較。可能現在你已經對上面的6點歸納有了一些認識。我們還可以通過JDK中得原始碼來認識一下具體hashCode和equals在程式碼中是如何呼叫的。

HashSet.java 

public boolean add(E e) {  
    return map.put(e, PRESENT)==null;  
    }

HashMap.java

複製程式碼

public V put(K key, V value) {  
        if (key == null)  
            return putForNullKey(value);  
        int hash = hash(key.hashCode());  
        int i = indexFor(hash, table.length);  
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
            Object k;  
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
                V oldValue = e.value;  
                e.value = value;  
                e.recordAccess(this);  
                return oldValue;  
            }  
        }  
  
        modCount++;  
        addEntry(hash, key, value, i);  
        return null;  
    }

複製程式碼

最後再來看幾個測試的例子吧:

  測試一:覆蓋equals(Object obj)但不覆蓋hashCode(),導致資料不唯一性

複製程式碼

public class HashCodeTest {  
    public static void main(String[] args) {  
        Collection set = new HashSet();  
        Point p1 = new Point(1, 1);  
        Point p2 = new Point(1, 1);  
  
        System.out.println(p1.equals(p2));  
        set.add(p1);   //(1)  
        set.add(p2);   //(2)  
        set.add(p1);   //(3)  
  
        Iterator iterator = set.iterator();  
        while (iterator.hasNext()) {  
            Object object = iterator.next();  
            System.out.println(object);  
        }  
    }  
}  
  
class Point {  
    private int x;  
    private int y;  
  
    public Point(int x, int y) {  
        super();  
        this.x = x;  
        this.y = y;  
    }  
  
    @Override  
    public boolean equals(Object obj) {  
        if (this == obj)  
            return true;  
        if (obj == null)  
            return false;  
        if (getClass() != obj.getClass())  
            return false;  
        Point other = (Point) obj;  
        if (x != other.x)  
            return false;  
        if (y != other.y)  
            return false;  
        return true;  
    }  
  
    @Override  
    public String toString() {  
        return "x:" + x + ",y:" + y;  
    }  
  
}  

複製程式碼

  輸出結果:

true  
x:1,y:1  
x:1,y:1  

  原因分析:

(1)當執行set.add(p1)時(1),集合為空,直接存入集合;

(2)當執行set.add(p2)時(2),首先判斷該物件(p2)的hashCode值所在的儲存區域是否有相同的hashCode,因為沒有覆蓋hashCode方法,所以jdk使用預設Object的hashCode方法,返回記憶體地址轉換後的整數,因為不同物件的地址值不同,所以這裡不存在與p2相同hashCode值的物件,因此jdk預設不同hashCode值,equals一定返回false,所以直接存入集合。

 (3)當執行set.add(p1)時(3),時,因為p1已經存入集合,同一物件返回的hashCode值是一樣的,繼續判斷equals是否返回true,因為是同一物件所以返回true。此時jdk認為該物件已經存在於集合中,所以捨棄。

  測試二:覆蓋hashCode方法,但不覆蓋equals方法,仍然會導致資料的不唯一性

修改Point類:

複製程式碼

class Point {  
    private int x;  
    private int y;  
  
    public Point(int x, int y) {  
        super();  
        this.x = x;  
        this.y = y;  
    }  
  
    @Override  
    public int hashCode() {  
        final int prime = 31;  
        int result = 1;  
        result = prime * result + x;  
        result = prime * result + y;  
        return result;  
    }  
  
    @Override  
    public String toString() {  
        return "x:" + x + ",y:" + y;  
    }  
  
}  

複製程式碼

輸出結果:

false  
x:1,y:1  
x:1,y:1

原因分析:

(1)當執行set.add(p1)時(1),集合為空,直接存入集合;

(2)當執行set.add(p2)時(2),首先判斷該物件(p2)的hashCode值所在的儲存區域是否有相同的hashCode,這裡覆蓋了hashCode方法,p1和p2的hashCode相等,所以繼續判斷equals是否相等,因為這裡沒有覆蓋equals,預設使用'=='來判斷,所以這裡equals返回false,jdk認為是不同的物件,所以將p2存入集合。

 (3)當執行set.add(p1)時(3),時,因為p1已經存入集合,同一物件返回的hashCode值是一樣的,並且equals返回true。此時jdk認為該物件已經存在於集合中,所以捨棄。

綜合上述兩個測試,要想保證元素的唯一性,必須同時覆蓋hashCode和equals才行。

(注意:在HashSet中插入同一個元素(hashCode和equals均相等)時,會被捨棄,而在HashMap中插入同一個Key(Value 不同)時,原來的元素會被覆蓋。)

測試三:在記憶體洩露問題

複製程式碼

public class HashCodeTest {  
    public static void main(String[] args) {  
        Collection set = new HashSet();  
        Point p1 = new Point(1, 1);  
        Point p2 = new Point(1, 2);  
  
        set.add(p1);  
        set.add(p2);  
          
        p2.setX(10);  
        p2.setY(10);  
          
        set.remove(p2);  
  
        Iterator iterator = set.iterator();  
        while (iterator.hasNext()) {  
            Object object = iterator.next();  
            System.out.println(object);  
        }  
    }  
}  
  
class Point {  
    private int x;  
    private int y;  
  
    public Point(int x, int y) {  
        super();  
        this.x = x;  
        this.y = y;  
    }  
  
  
    public int getX() {  
        return x;  
    }  
  
  
    public void setX(int x) {  
        this.x = x;  
    }  
  
  
    public int getY() {  
        return y;  
    }  
  
  
    public void setY(int y) {  
        this.y = y;  
    }  
  
  
    @Override  
    public int hashCode() {  
        final int prime = 31;  
        int result = 1;  
        result = prime * result + x;  
        result = prime * result + y;  
        return result;  
    }  
  
  
    @Override  
    public boolean equals(Object obj) {  
        if (this == obj)  
            return true;  
        if (obj == null)  
            return false;  
        if (getClass() != obj.getClass())  
            return false;  
        Point other = (Point) obj;  
        if (x != other.x)  
            return false;  
        if (y != other.y)  
            return false;  
        return true;  
    }  
  
  
    @Override  
    public String toString() {  
        return "x:" + x + ",y:" + y;  
    }  
  
}  

複製程式碼

執行結果:

x:1,y:1  
x:10,y:10

原因分析:

    假設p1的hashCode為1,p2的hashCode為2,在儲存時p1被分配在1號桶中,p2被分配在2號筒中。這時修改了p2中與計算hashCode有關的資訊(x和y),當呼叫remove(Object obj)時,首先會查詢該hashCode值得物件是否在集合中。假設修改後的hashCode值為10(仍存在2號桶中),這時查詢結果空,jdk認為該物件不在集合中,所以不會進行刪除操作。然而使用者以為該物件已經被刪除,導致該物件長時間不能被釋放,造成記憶體洩露。解決該問題的辦法是不要在執行期間修改與hashCode值有關的物件資訊,如果非要修改,則必須先從集合中刪除,更新資訊後再加入集合中。

總結:   1.hashCode是為了提高在雜湊結構儲存中查詢的效率,線上性表中沒有作用。   2.equals和hashCode需要同時覆蓋。   3.若兩個物件equals返回true,則hashCode有必要也返回相同的int數。   4.若兩個物件equals返回false,則hashCode不一定返回不同的int數,但為不相等的物件生成不同hashCode值可以提高 雜湊表的效能。   5.若兩個物件hashCode返回相同int數,則equals不一定返回true。   6.若兩個物件hashCode返回不同int數,則equals一定返回false。   7.同一物件在執行期間若已經儲存在集合中,則不能修改影響hashCode值的相關資訊,否則會導致記憶體洩露問題。