欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Java hashCode原理以及與equals()區(qū)別聯(lián)系詳解

 更新時間:2022年11月29日 11:53:30   作者:小蝦仁蕪湖  
在 Java 應(yīng)用程序執(zhí)行期間,在同一對象上多次調(diào)用 hashCode 方法時,必須一致地返回相同的整數(shù),前提是對象上 equals 比較中所用的信息沒有被修改。從某一應(yīng)用程序的一次執(zhí)行到同一應(yīng)用程序的另一次執(zhí)行,該整數(shù)無需保持一致

1、什么是hashCode

hashCode就是對象的散列碼,是根據(jù)對象的某些信息推導(dǎo)出的一個整數(shù)值,默認(rèn)情況下表示是對象的存儲地址。通過散列碼,可以提高檢索的效率,主要用于在散列存儲結(jié)構(gòu)中快速確定對象的存儲地址,如Hashtable、hashMap中。

為什么說hashcode可以提高檢索效率呢?我們先看一個例子,如果想判斷一個集合是否包含某個對象,最簡單的做法是怎樣的呢?逐一取出集合中的每個元素與要查找的對象進(jìn)行比較,當(dāng)發(fā)現(xiàn)該元素與要查找的對象進(jìn)行equals()比較的結(jié)果為true時,則停止繼續(xù)查找并返回true,否則,返回false。如果一個集合中有很多個元素,比如有一萬個元素,并且沒有包含要查找的對象時,則意味著你的程序需要從集合中取出一萬個元素進(jìn)行逐一比較才能得到結(jié)論,這樣做的效率是非常低的。這時,可以采用哈希算法(散列算法)來提高從集合中查找元素的效率,將數(shù)據(jù)按特定算法直接分配到不同區(qū)域上。將集合分成若干個存儲區(qū)域,每個對象可以計算出一個哈希碼,可以將哈希碼分組(使用不同的hash函數(shù)來計算的),每組分別對應(yīng)某個存儲區(qū)域,根據(jù)一個對象的哈希碼就可以確定該對象應(yīng)該存儲在哪個區(qū)域,大大減少查詢匹配元素的數(shù)量。

比如HashSet就是采用哈希算法存取對象的集合,它內(nèi)部采用對某個數(shù)字n進(jìn)行取余的方式對哈希碼進(jìn)行分組和劃分對象的存儲區(qū)域,當(dāng)從HashSet集合中查找某個對象時,Java系統(tǒng)首先調(diào)用對象的hashCode()方法獲得該對象的哈希碼,然后根據(jù)哈希嗎找到相應(yīng)的存儲區(qū)域,最后取得該存儲區(qū)域內(nèi)的每個元素與該對象進(jìn)行equals()比較,這樣就不用遍歷集合中的所有元素就可以得到結(jié)論。

下面通過String類的hashCode()計算一組散列碼:

public class HashCodeTest {
	public static void main(String[] args) {
		int hash= 0;
		String s= "ok";
		StringBuilder sb = new StringBuilder(s);
		System.out.println(s.hashCode() + "  " + sb.hashCode());
		String t = new String("ok");
		StringBuilder tb =new StringBuilder(s);
		System.out.println(t.hashCode() + "  " + tb.hashCode());
	}
}

運行結(jié)果:
3548  1829164700
3548  2018699554

我們可以看出,字符串s與t擁有相同的散列碼,這是因為字符串的散列碼是由內(nèi)容導(dǎo)出的。而字符串緩沖sb與tb卻有著不同的散列碼,這是因為StringBuilder沒有重寫hashCode()方法,它的散列碼是由Object類默認(rèn)的hashCode()計算出來的對象存儲地址,所以散列碼自然也就不同了。那么該如何重寫出一個較好的hashCode方法呢,其實并不難,我們只要合理地組織對象的散列碼,就能夠讓不同的對象產(chǎn)生比較均勻的散列碼。例如下面的例子:

public class Model {
	private String name;
	private double salary;
	private int sex;
	@Override
	public int hashCode() {
		return name.hashCode() + new Double(salary).hashCode() + new Integer(sex).hashCode();
	}
}

上面的代碼我們通過合理的利用各個屬性對象的散列碼進(jìn)行組合,最終便能產(chǎn)生一個相對比較好的或者說更加均勻的散列碼,當(dāng)然上面僅僅是個參考例子而已,我們也可以通過其他方式去實現(xiàn),只要能使散列碼更加均勻(所謂的均勻就是每個對象產(chǎn)生的散列碼最好都不沖突)就行了。不過這里有點要注意的就是java 7中對hashCode方法做了兩個改進(jìn),首先java發(fā)布者希望我們使用更加安全的調(diào)用方式來返回散列碼,也就是使用null安全的方法Objects.hashCode(注意不是Object而是java.util.Objects)方法,這個方法的優(yōu)點是如果參數(shù)為null,就只返回0,否則返回對象參數(shù)調(diào)用的hashCode的結(jié)果。Objects.hashCode 源碼如下:

public static int hashCode(Object o) {
        return o != null ? o.hashCode() : 0;
    }

因此我們修改后的代碼如下:

import java.util.Objects;
public  class Model {
	private   String name;
	private double salary;
	private int sex;
	@Override
	public int hashCode() {
		return Objects.hashCode(name) + new Double(salary).hashCode() + new Integer(sex).hashCode();
	}
}

java 7還提供了另外一個方法java.util.Objects.hash(Object… objects),當(dāng)我們需要組合多個散列值時可以調(diào)用該方法。進(jìn)一步簡化上述的代碼:

import java.util.Objects;
public  class Model {
	private   String name;
	private double salary;
	private int sex;
	@Override
	public int hashCode() {
		return Objects.hash(name,salary,sex);
	}
}

好了,到此hashCode()該介紹的我們都說了,還有一點要說的,如果我們提供的是一個數(shù)組類型的變量的話,那么我們可以調(diào)用Arrays.hashCode()來計算它的散列碼,這個散列碼是由數(shù)組元素的散列碼組成的。

2、equals()與hashCode()的聯(lián)系

Java的超類Object類已經(jīng)定義了equals()和hashCode()方法,在Obeject類中,equals()比較的是兩個對象的內(nèi)存地址是否相等,而hashCode()返回的是對象的內(nèi)存地址。所以hashCode主要是用于查找使用的,而equals()是用于比較兩個對象是否相等的。但有時候我們根據(jù)特定的需求,可能要重寫這兩個方法,在重寫這兩個方法的時候,主要注意保持一下幾個特性:

(1)如果兩個對象的equals()結(jié)果為true,那么這兩個對象的hashCode一定相同;

(2)兩個對象的hashCode()結(jié)果相同,并不能代表兩個對象的equals()一定為true,只能夠說明這兩個對象在一個散列存儲結(jié)構(gòu)中。

(3)如果對象的equals()被重寫,那么對象的hashCode()也要重寫。

3、為什么重寫equals()的同時要重寫hashCode()方法

在將這個問題的答案之前,我們先了解一下將元素放入集合的流程,如下圖:

將對象放入到集合中時,首先判斷要放入對象的hashcode值與集合中的任意一個元素的hashcode值是否相等,如果不相等直接將該對象放入集合中。如果hashcode值相等,然后再通過equals()判斷要放入對象與該存儲區(qū)域的任意一個對象是否相等,如果equals()判斷不相等,直接將該元素放入到集合中,否則不放入。

同樣,在使用get()查詢元素的時候,集合類也先調(diào)key.hashCode()算出數(shù)組下標(biāo),然后看equals()的結(jié)果,如果是true就是找到了,否則就是沒找到。

假設(shè)我們我們重寫了對象的equals(),但是不重寫hashCode()方法,由于超類Object中的hashcode()方法始終返回的是一個對象的內(nèi)存地址,而不同對象的這個內(nèi)存地址永遠(yuǎn)是不相等的。這時候,即使我們重寫了equals()方法,也不會有特定的效果的,因為不能確保兩個equals()結(jié)果為true的兩個對象會被散列在同一個存儲區(qū)域,即 obj1.equals(obj2) 的結(jié)果為true,但是不能保證 obj1.hashCode() == obj2.hashCode() 表達(dá)式的結(jié)果也為true;這種情況,就會導(dǎo)致數(shù)據(jù)出現(xiàn)不唯一,因為如果連hashCode()都不相等的話,就不會調(diào)用equals方法進(jìn)行比較了,所以重寫equals()就沒有意義了。

以HashSet為例,如果一個類的hashCode()方法沒有遵循上述要求,那么當(dāng)這個類的兩個實例對象用equals()方法比較的結(jié)果相等時,他們本來應(yīng)該無法被同時存儲進(jìn)set集合中,但是,如果將他們存儲進(jìn)HashSet集合中時,由于他們的hashCode()方法的返回值不同(HashSet使用的是Object中的hashCode(),它返回值是對象的內(nèi)存地址),第二個對象首先按照哈希碼計算可能被放進(jìn)與第一個對象不同的區(qū)域中,這樣,它就不可能與第一個對象進(jìn)行equals方法比較了,也就可能被存儲進(jìn)HashSet集合中了;所以,Object類中的hashCode()方法不能滿足對象被存入到HashSet中的要求,因為它的返回值是通過對象的內(nèi)存地址推算出來的,同一個對象在程序運行期間的任何時候返回的哈希值都是始終不變的,所以,只要是兩個不同的實例對象,即使他們的equals方法比較結(jié)果相等,他們默認(rèn)的hashCode方法的返回值是不同的。

接下來,我們就舉幾個小例子測試一下:

3.1、測試一

覆蓋equals()但不覆蓋hashCode(),導(dǎo)致數(shù)據(jù)不唯一性。

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

輸出結(jié)果:
true
x:1,y:1  
x:1,y:1 

原因分析:

  • 當(dāng)執(zhí)行set.add(p1)時(1),集合為空,直接存入集合;
  • 當(dāng)執(zhí)行set.add(p2)時(2),首先判斷該對象p2的hashCode值所在的存儲區(qū)域是否有相同的hashCode,因為沒有覆蓋hashCode方法,所以默認(rèn)使用Object的hashCode方法,返回內(nèi)存地址轉(zhuǎn)換后的整數(shù),因為不同對象的地址值不同,所以這里不存在與p2相同hashCode值的對象,所以直接存入集合。
  • 當(dāng)執(zhí)行set.add(p1)時(3),時,因為p1已經(jīng)存入集合,同一對象返回的hashCode值是一樣的,繼續(xù)判斷equals是否返回true,因為是同一對象所以返回true。此時jdk認(rèn)為該對象已經(jīng)存在于集合中,所以舍棄。

3.2、測試二

覆蓋hashCode(),但不覆蓋equals(),仍然會導(dǎo)致數(shù)據(jù)的不唯一性。

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

輸出結(jié)果:
false
x:1,y:1  
x:1,y:1 

原因分析:

  • 當(dāng)執(zhí)行set.add(p1)時(1),集合為空,直接存入集合;
  • 當(dāng)執(zhí)行set.add(p2)時(2),首先判斷該對象p2的hashCode值所在的存儲區(qū)域是否有相同的hashCode,這里覆蓋了hashCode方法,p1和p2的hashCode相等,所以繼續(xù)判斷equals()是否相等,因為這里沒有覆蓋equals(),默認(rèn)使用 “” 來判斷,而 “” 比較的是兩個對象的內(nèi)存地址,所以這里equals()會返回false,所以集合認(rèn)為是不同的對象,所以將p2存入集合。
  • 當(dāng)執(zhí)行set.add(p1)時(3),時,因為p1已經(jīng)存入集合,同一對象返回的hashCode值是一樣的,并且equals返回true。此時認(rèn)為該對象已經(jīng)存在于集合中,所以舍棄。

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

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

4、由hashCode()造成的內(nèi)存泄露問題

public class RectObject {
	public int x;
	public int y;
	public RectObject(int x,int y){
		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 boolean equals(Object obj){
		if(this == obj)
			return true;
		if(obj == null)
			return false;
		if(getClass() != obj.getClass())
			return false;
		final RectObject other = (RectObject)obj;
		if(x != other.x){
			return false;
		}
		if(y != other.y){
			return false;
		}
		return true;
	}
}

我們重寫了父類Object中的hashCode和equals方法,看到hashCode和equals方法中,如果兩個RectObject對象的x,y值相等的話他們的hashCode值是相等的,同時equals返回的是true;

import java.util.HashSet;
public class Demo {
	public static void main(String[] args){
		HashSet<RectObject> set = new HashSet<RectObject>();
		RectObject r1 = new RectObject(3,3);
		RectObject r2 = new RectObject(5,5);
		RectObject r3 = new RectObject(3,3);
		set.add(r1);
		set.add(r2);
		set.add(r3);
		r3.y = 7;
		System.out.println("刪除前的大小size:"+set.size());//2
		set.remove(r3);
		System.out.println("刪除后的大小size:"+set.size());//2
	}
}

運行結(jié)果:
刪除前的大小size:3
刪除后的大小size:3

在這里,我們發(fā)現(xiàn)了一個問題,當(dāng)我們調(diào)用了remove刪除r3對象,以為刪除了r3,但事實上并沒有刪除,這就叫做內(nèi)存泄露,就是不用的對象但是他還在內(nèi)存中。所以我們多次這樣操作之后,內(nèi)存就爆了??匆幌聄emove的源碼:

    public boolean remove(Object o) {
        return map.remove(o)==PRESENT;
    }

然后再看一下map的remove方法的源碼:

    public V remove(Object key) {
        Entry<K,V> e = removeEntryForKey(key);
        return (e == null ? null : e.value);
    }

再看一下removeEntryForKey方法源碼:

/**
     * Removes and returns the entry associated with the specified key
     * in the HashMap.  Returns null if the HashMap contains no mapping
     * for this key.
     */
    final Entry<K,V> removeEntryForKey(Object key) {
        int hash = (key == null) ? 0 : hash(key);
        int i = indexFor(hash, table.length);
        Entry<K,V> prev = table[i];
        Entry<K,V> e = prev;
        while (e != null) {
            Entry<K,V> next = e.next;
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k)))) {
                modCount++;
                size--;
                if (prev == e)
                    table[i] = next;
                else
                    prev.next = next;
                e.recordRemoval(this);
                return e;
            }
            prev = e;
            e = next;
        }
        return e;
    }

我們看到,在調(diào)用remove方法的時候,會先使用對象的hashCode值去找到這個對象,然后進(jìn)行刪除,這種問題就是因為我們在修改了 r3 對象的 y 屬性的值,又因為RectObject對象的hashCode()方法中有y值參與運算,所以r3對象的hashCode就發(fā)生改變了,所以remove方法中并沒有找到 r3,所以刪除失敗。即 r3的hashCode變了,但是他存儲的位置沒有更新,仍然在原來的位置上,所以當(dāng)我們用他的新的hashCode去找肯定是找不到了.

上面的這個內(nèi)存泄露告訴我一個信息:如果我們將對象的屬性值參與了hashCode的運算中,在進(jìn)行刪除的時候,就不能對其屬性值進(jìn)行修改,否則會導(dǎo)致內(nèi)存泄露問題。

5、基本數(shù)據(jù)類型和String類型的hashCode()方法和equals()方法

(1)hashCode():八種基本類型的hashCode()很簡單就是直接返回他們的數(shù)值大小,String對象是通過一個復(fù)雜的計算方式,但是這種計算方式能夠保證,如果這個字符串的值相等的話,他們的hashCode就是相等的。

(2)equals():8種基本類型的equals方法就是直接比較數(shù)值,String類型的equals方法是比較字符串的值的。

到此這篇關(guān)于Java hashCode原理以及與equals()區(qū)別聯(lián)系詳解的文章就介紹到這了,更多相關(guān)Java hashCode內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • ElasticSearch啟動成功卻無法在瀏覽器訪問問題解決辦法

    ElasticSearch啟動成功卻無法在瀏覽器訪問問題解決辦法

    因工作的需要,要使用elasticsearch,安裝完了,啟動也成功了之后發(fā)現(xiàn)了問題,這篇文章主要給大家介紹了關(guān)于ElasticSearch啟動成功卻無法在瀏覽器訪問問題的解決辦法,需要的朋友可以參考下
    2024-10-10
  • java中toString()、String.valueOf()、(String)?強(qiáng)轉(zhuǎn)的區(qū)別

    java中toString()、String.valueOf()、(String)?強(qiáng)轉(zhuǎn)的區(qū)別

    在實際開發(fā)中,少不了使用這三種方法對某一個類型的數(shù)據(jù)進(jìn)行轉(zhuǎn)?String?的操作,本文就來介紹了java中toString()、String.valueOf()、(String)?強(qiáng)轉(zhuǎn)的區(qū)別,感興趣的可以了解一下
    2024-06-06
  • Eclipse不自動編譯java文件的終極解決方法

    Eclipse不自動編譯java文件的終極解決方法

    這篇文章主要介紹了Eclipse不自動編譯java文件的終極解決方法,需要的朋友可以參考下
    2015-12-12
  • java遞歸生成樹型結(jié)構(gòu)方式

    java遞歸生成樹型結(jié)構(gòu)方式

    文章介紹了如何使用Java遞歸生成樹形結(jié)構(gòu),包括獲取數(shù)據(jù)、生成樹形結(jié)構(gòu)、查詢子節(jié)點等步驟,作者分享了自己的經(jīng)驗,希望能對大家有所幫助
    2024-12-12
  • Java上傳文件大小受限問題的解決方法

    Java上傳文件大小受限問題的解決方法

    這篇文章主要介紹了Java上傳文件大小受限怎么解決,本文給大家分享問題分析及解決方案,需要的朋友可以參考下
    2023-09-09
  • 在Spring中如何使用動態(tài)代理?

    在Spring中如何使用動態(tài)代理?

    上篇文章記錄自定義切面,下邊記錄使用注解來編寫自定義切面,文中有非常詳細(xì)的介紹及代碼示例,需要的朋友可以參考下
    2021-06-06
  • Spring Boot如何優(yōu)化內(nèi)嵌的Tomcat示例詳解

    Spring Boot如何優(yōu)化內(nèi)嵌的Tomcat示例詳解

    spring boot默認(rèn)web程序啟用tomcat內(nèi)嵌容器,監(jiān)聽8080端口,下面這篇文章主要給大家介紹了關(guān)于Spring Boot如何優(yōu)化內(nèi)嵌Tomcat的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考借鑒,下面來一起看看吧。
    2017-09-09
  • 實例化JFileChooser對象報空指針異常問題的解決辦法

    實例化JFileChooser對象報空指針異常問題的解決辦法

    今天小編就為大家分享一篇關(guān)于實例化JFileChooser對象報空指針異常問題的解決辦法,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧
    2019-02-02
  • struts2中使用注解配置Action方法詳解

    struts2中使用注解配置Action方法詳解

    這篇文章主要介紹了struts2中使用注解配置Action方法詳解,涉及一個示例,具有一定參考價值,需要的朋友可以了解下。
    2017-10-10
  • Java之理解多態(tài)詳解

    Java之理解多態(tài)詳解

    大家好,本篇文章主要講的是Java之理解多態(tài)詳解,感興趣的同學(xué)趕快來看一看吧,對你有幫助的話記得收藏一下,方便下次瀏覽
    2021-12-12

最新評論