JAVA基礎 & hashCode與equals詳解

在瞭解hashCode與equals方法之前,需要知道:

1、JAVA八種基本數據類型的變量沒有equals和hashcode方法,只能應用==進行比較;

2、變量1==變量2,比較的是對象在堆內存中的地址,如果要比較其中的內容的話,就要用equals方法(前提是在自己定義的對象中重寫了equals方法,如果沒有重寫equals方法使用equals比較和==的意義是相同的,都是比較對象存放的地址,因爲Object 的equals方法,就是用==來實現的)。

學習hashCode與equals方法之前先來試想一個場景,如果你想查找一個集合中是否包含某個對象,那麼程序應該怎麼寫呢?

通常的做法是逐一取出每個元素與要查找的對象一 一比較,當發現兩者進行equals比較結果相等時,則停止查找並返回true,否則,返回false。但是這個做法的一個缺點是在需要進行大量並且快速的對象對比時,如果都用equal()去做顯然效率太低。於是有人發明了一種哈希算法來提高從該集合中查找元素的效率,這種方式將集合分成若干個存儲區域(可以看成一個個桶),每個對象可以計算出一個哈希碼,可以根據哈希碼分組,每組分別對應某個存儲區域,這樣一個對象根據它的哈希碼就可以分到不同的存儲區域(不同的桶中)。如下圖所示:

所以解決方式是,每當需要對比的時候,首先用hashCode()去對比,如果hashCode()不一樣,則表示這兩個對象肯定不相等(也就是不必再用equal()去再對比了)。如果hashCode()相同,此時再對比他們的equal(),如果equal()也相同,則表示這兩個對象是真的相同了,這樣既能大大提高了效率也保證了對比的絕對正確性!

實際的使用中,一個對象一般有key和value,可以根據key來計算對象的hashCode。假設現在全部的對象都已經根據自己的hashCode值存儲在不同的存儲區域中了,那麼查找某個對象(根據對象的key來查找),不需要遍歷整個集合了,現在只需要計算要查找對象的key的hashCode,然後找到該hashCode對應的存儲區域,在該存儲區域中來查找就可以了。

這種大量的並且快速的對象對比的使用,一般是在hash容器中,比如HashSet,HashMap,HashTable等。比如HashSet裏對象無序且不可重複,當添加對象時,它內部必然要對添加進去的每個對象進行對比,而它的對比規則就是像上面說的那樣,首先比較兩個對象的hashcode值,如果hashcode值相同,則再通過equals比較,再相同的話才能確定是同一個對象,不添加。否則就可以添加進HashSet。 而HashMap,通過一個object的key來取HashMap的value時,工作方法是,通過傳入的Object的hashCode在內存中找地址,當找到這個地址後再通過equals方法來比較這個地址中的內容是否和你原來放進去的一樣,如果是,就取出value。

這樣一來,對比的效率就很高了。

 

那麼hashCode()既然效率這麼高爲什麼還要equal()呢?

       因爲hashCode()並不是完全可靠,有時候不同的對象他們生成的hashcode也會一樣(生成hash值得公式可能存在的問題),所以hashCode()只能說是大部分時候可靠,並不是絕對可靠,所以可以得出:

1.equal()相等的兩個對象他們的hashCode()肯定相等,也就是用equal()對比是絕對可靠的。

2.hashCode()相等的兩個對象他們的equal()不一定相等,也就是hashCode()不是絕對可靠的。

 

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

在研究這個問題之前,首先說明一下JDK對equals(Object obj)和hashCode()這兩個方法的定義和規範。

在Java中任何一個對象都具備equals(Object obj)和hashCode()這兩個方法,因爲他們是在Object類中定義的。 

equals(Object obj)方法用來判斷兩個對象是否“相同”,如果“相同”則返回true,否則返回false。 

hashCode()方法返回一個int數,在Object類中的默認實現是“將該對象的內部地址轉換成一個整數返回”。 

舉例說明一下:

兩個Man對象,存儲內容一致,都沒有重寫hashcode和equals方法

                Man man = new Man("liming", 25, "lili");
		Man man1 = new Man("liming", 25, "lili");
		Set<Man> set = new HashSet<Man>();
		set.add(man);
		set.add(man1);
                System.out.println(set.size());

輸出:2

兩個man對象,存儲內容不一致,但是自定義了hashcode和equals比較規則

        @Override
	public int hashCode()
	{
		// TODO Auto-generated method stub
		return 123;
	}
 
	@Override
	public boolean equals(Object obj)
	{
		Man man = (Man) obj;
		return man.age == this.age;
	}
                Man man = new Man("liming", 25, "lili");
		Man man1 = new Man("limingasd", 25, "liliasd");
		Set<Man> set = new HashSet<Man>();
		set.add(man);
		set.add(man1);
		System.out.println(set.size());

 輸出:1

下面是官方文檔給出的一些說明:

hashCode 的常規協定是:   
在 Java 應用程序執行期間,在同一對象上多次調用 hashCode 方法時,必須一致地返回相同的整數,前提是對象上 equals 比較中所用的信息沒有被修改。從某一應用程序的一次執行到同一應用程序的另一次執行,該整數無需保持一致。   
如果根據 equals(Object) 方法,兩個對象是相等的,那麼在兩個對象中的每個對象上調用 hashCode 方法都必須生成相同的整數結果。   
以下情況不 是必需的:如果根據 equals(java.lang.Object) 方法,兩個對象不相等,那麼在兩個對象中的任一對象上調用 hashCode 方法必定會生成不同的整數結果。但是,程序員應該知道,爲不相等的對象生成不同整數結果可以提高哈希表的性能。   
實際上,由 Object 類定義的 hashCode 方法確實會針對不同的對象返回不同的整數。(這一般是通過將該對象的內部地址轉換成一個整數來實現的,但是 JavaTM 編程語言不需要這種實現技巧。)   
  
當equals方法被重寫時,通常有必要重寫 hashCode 方法,以維護 hashCode 方法的常規協定,該協定聲明相等對象必須具有相等的哈希碼。

查閱了相關資料之後對以上的說明做的歸納總結:
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,所以重寫了也不要緊,也爲後期的修改打了補丁。
 

對象放入散列集合的流程圖:

 

從上面的圖中可以清晰地看到在存儲一個對象時,先進行hashCode值的比較,然後進行equals的比較。

我們還可以通過JDK中得源碼來認識一下具體hashCode和equals在代碼中是如何調用的。

HashSet中add方法源代碼:

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

map.put源代碼:

    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、equals方法用於比較對象的內容是否相等(覆蓋以後)

       2、hashCode是爲了提高在散列結構存儲中查找的效率,只有在集合中用到,在線性表中沒有作用。

       3、equals和hashCode需要同時覆蓋。

       4、若兩個對象equals返回true,則hashCode有必要也返回相同的int數。

       5、若兩個對象equals返回false,則hashCode不一定返回不同的int數,但爲不相等的對象生成不同hashCode值可以提高哈希表的性能。

       6、若兩個對象hashCode返回相同int數,則equals不一定返回true。

       7、若兩個對象hashCode返回不同int數,則equals一定返回false。

       8、同一對象在執行期間若已經存儲在集合中,則不能修改影響hashCode值的相關信息,否則會導致內存泄露問題。
 

參考文章:

https://blog.csdn.net/bailu666666/article/details/81153815

https://blog.csdn.net/lijiecao0226/article/details/24609559

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