Java hashCode原理以及與equals()區(qū)別聯(lián)系詳解
1、什么是hashCode
hashCode就是對(duì)象的散列碼,是根據(jù)對(duì)象的某些信息推導(dǎo)出的一個(gè)整數(shù)值,默認(rèn)情況下表示是對(duì)象的存儲(chǔ)地址。通過散列碼,可以提高檢索的效率,主要用于在散列存儲(chǔ)結(jié)構(gòu)中快速確定對(duì)象的存儲(chǔ)地址,如Hashtable、hashMap中。
為什么說hashcode可以提高檢索效率呢?我們先看一個(gè)例子,如果想判斷一個(gè)集合是否包含某個(gè)對(duì)象,最簡(jiǎn)單的做法是怎樣的呢?逐一取出集合中的每個(gè)元素與要查找的對(duì)象進(jìn)行比較,當(dāng)發(fā)現(xiàn)該元素與要查找的對(duì)象進(jìn)行equals()比較的結(jié)果為true時(shí),則停止繼續(xù)查找并返回true,否則,返回false。如果一個(gè)集合中有很多個(gè)元素,比如有一萬個(gè)元素,并且沒有包含要查找的對(duì)象時(shí),則意味著你的程序需要從集合中取出一萬個(gè)元素進(jìn)行逐一比較才能得到結(jié)論,這樣做的效率是非常低的。這時(shí),可以采用哈希算法(散列算法)來提高從集合中查找元素的效率,將數(shù)據(jù)按特定算法直接分配到不同區(qū)域上。將集合分成若干個(gè)存儲(chǔ)區(qū)域,每個(gè)對(duì)象可以計(jì)算出一個(gè)哈希碼,可以將哈希碼分組(使用不同的hash函數(shù)來計(jì)算的),每組分別對(duì)應(yīng)某個(gè)存儲(chǔ)區(qū)域,根據(jù)一個(gè)對(duì)象的哈希碼就可以確定該對(duì)象應(yīng)該存儲(chǔ)在哪個(gè)區(qū)域,大大減少查詢匹配元素的數(shù)量。
比如HashSet就是采用哈希算法存取對(duì)象的集合,它內(nèi)部采用對(duì)某個(gè)數(shù)字n進(jìn)行取余的方式對(duì)哈希碼進(jìn)行分組和劃分對(duì)象的存儲(chǔ)區(qū)域,當(dāng)從HashSet集合中查找某個(gè)對(duì)象時(shí),Java系統(tǒng)首先調(diào)用對(duì)象的hashCode()方法獲得該對(duì)象的哈希碼,然后根據(jù)哈希嗎找到相應(yīng)的存儲(chǔ)區(qū)域,最后取得該存儲(chǔ)區(qū)域內(nèi)的每個(gè)元素與該對(duì)象進(jìn)行equals()比較,這樣就不用遍歷集合中的所有元素就可以得到結(jié)論。
下面通過String類的hashCode()計(jì)算一組散列碼:
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()); } }
運(yùn)行結(jié)果:
3548 1829164700
3548 2018699554
我們可以看出,字符串s與t擁有相同的散列碼,這是因?yàn)樽址纳⒘写a是由內(nèi)容導(dǎo)出的。而字符串緩沖sb與tb卻有著不同的散列碼,這是因?yàn)镾tringBuilder沒有重寫hashCode()方法,它的散列碼是由Object類默認(rèn)的hashCode()計(jì)算出來的對(duì)象存儲(chǔ)地址,所以散列碼自然也就不同了。那么該如何重寫出一個(gè)較好的hashCode方法呢,其實(shí)并不難,我們只要合理地組織對(duì)象的散列碼,就能夠讓不同的對(duì)象產(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(); } }
上面的代碼我們通過合理的利用各個(gè)屬性對(duì)象的散列碼進(jìn)行組合,最終便能產(chǎn)生一個(gè)相對(duì)比較好的或者說更加均勻的散列碼,當(dāng)然上面僅僅是個(gè)參考例子而已,我們也可以通過其他方式去實(shí)現(xiàn),只要能使散列碼更加均勻(所謂的均勻就是每個(gè)對(duì)象產(chǎn)生的散列碼最好都不沖突)就行了。不過這里有點(diǎn)要注意的就是java 7中對(duì)hashCode方法做了兩個(gè)改進(jìn),首先java發(fā)布者希望我們使用更加安全的調(diào)用方式來返回散列碼,也就是使用null安全的方法Objects.hashCode(注意不是Object而是java.util.Objects)方法,這個(gè)方法的優(yōu)點(diǎn)是如果參數(shù)為null,就只返回0,否則返回對(duì)象參數(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還提供了另外一個(gè)方法java.util.Objects.hash(Object… objects),當(dāng)我們需要組合多個(gè)散列值時(shí)可以調(diào)用該方法。進(jìn)一步簡(jiǎ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()該介紹的我們都說了,還有一點(diǎn)要說的,如果我們提供的是一個(gè)數(shù)組類型的變量的話,那么我們可以調(diào)用Arrays.hashCode()來計(jì)算它的散列碼,這個(gè)散列碼是由數(shù)組元素的散列碼組成的。
2、equals()與hashCode()的聯(lián)系
Java的超類Object類已經(jīng)定義了equals()和hashCode()方法,在Obeject類中,equals()比較的是兩個(gè)對(duì)象的內(nèi)存地址是否相等,而hashCode()返回的是對(duì)象的內(nèi)存地址。所以hashCode主要是用于查找使用的,而equals()是用于比較兩個(gè)對(duì)象是否相等的。但有時(shí)候我們根據(jù)特定的需求,可能要重寫這兩個(gè)方法,在重寫這兩個(gè)方法的時(shí)候,主要注意保持一下幾個(gè)特性:
(1)如果兩個(gè)對(duì)象的equals()結(jié)果為true,那么這兩個(gè)對(duì)象的hashCode一定相同;
(2)兩個(gè)對(duì)象的hashCode()結(jié)果相同,并不能代表兩個(gè)對(duì)象的equals()一定為true,只能夠說明這兩個(gè)對(duì)象在一個(gè)散列存儲(chǔ)結(jié)構(gòu)中。
(3)如果對(duì)象的equals()被重寫,那么對(duì)象的hashCode()也要重寫。
3、為什么重寫equals()的同時(shí)要重寫hashCode()方法
在將這個(gè)問題的答案之前,我們先了解一下將元素放入集合的流程,如下圖:
將對(duì)象放入到集合中時(shí),首先判斷要放入對(duì)象的hashcode值與集合中的任意一個(gè)元素的hashcode值是否相等,如果不相等直接將該對(duì)象放入集合中。如果hashcode值相等,然后再通過equals()判斷要放入對(duì)象與該存儲(chǔ)區(qū)域的任意一個(gè)對(duì)象是否相等,如果equals()判斷不相等,直接將該元素放入到集合中,否則不放入。
同樣,在使用get()查詢?cè)氐臅r(shí)候,集合類也先調(diào)key.hashCode()算出數(shù)組下標(biāo),然后看equals()的結(jié)果,如果是true就是找到了,否則就是沒找到。
假設(shè)我們我們重寫了對(duì)象的equals(),但是不重寫hashCode()方法,由于超類Object中的hashcode()方法始終返回的是一個(gè)對(duì)象的內(nèi)存地址,而不同對(duì)象的這個(gè)內(nèi)存地址永遠(yuǎn)是不相等的。這時(shí)候,即使我們重寫了equals()方法,也不會(huì)有特定的效果的,因?yàn)椴荒艽_保兩個(gè)equals()結(jié)果為true的兩個(gè)對(duì)象會(huì)被散列在同一個(gè)存儲(chǔ)區(qū)域,即 obj1.equals(obj2) 的結(jié)果為true,但是不能保證 obj1.hashCode() == obj2.hashCode() 表達(dá)式的結(jié)果也為true;這種情況,就會(huì)導(dǎo)致數(shù)據(jù)出現(xiàn)不唯一,因?yàn)槿绻BhashCode()都不相等的話,就不會(huì)調(diào)用equals方法進(jìn)行比較了,所以重寫equals()就沒有意義了。
以HashSet為例,如果一個(gè)類的hashCode()方法沒有遵循上述要求,那么當(dāng)這個(gè)類的兩個(gè)實(shí)例對(duì)象用equals()方法比較的結(jié)果相等時(shí),他們本來應(yīng)該無法被同時(shí)存儲(chǔ)進(jìn)set集合中,但是,如果將他們存儲(chǔ)進(jìn)HashSet集合中時(shí),由于他們的hashCode()方法的返回值不同(HashSet使用的是Object中的hashCode(),它返回值是對(duì)象的內(nèi)存地址),第二個(gè)對(duì)象首先按照哈希碼計(jì)算可能被放進(jìn)與第一個(gè)對(duì)象不同的區(qū)域中,這樣,它就不可能與第一個(gè)對(duì)象進(jìn)行equals方法比較了,也就可能被存儲(chǔ)進(jìn)HashSet集合中了;所以,Object類中的hashCode()方法不能滿足對(duì)象被存入到HashSet中的要求,因?yàn)樗姆祷刂凳峭ㄟ^對(duì)象的內(nèi)存地址推算出來的,同一個(gè)對(duì)象在程序運(yùn)行期間的任何時(shí)候返回的哈希值都是始終不變的,所以,只要是兩個(gè)不同的實(shí)例對(duì)象,即使他們的equals方法比較結(jié)果相等,他們默認(rèn)的hashCode方法的返回值是不同的。
接下來,我們就舉幾個(gè)小例子測(cè)試一下:
3.1、測(cè)試一
覆蓋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)時(shí)(1),集合為空,直接存入集合;
- 當(dāng)執(zhí)行set.add(p2)時(shí)(2),首先判斷該對(duì)象p2的hashCode值所在的存儲(chǔ)區(qū)域是否有相同的hashCode,因?yàn)闆]有覆蓋hashCode方法,所以默認(rèn)使用Object的hashCode方法,返回內(nèi)存地址轉(zhuǎn)換后的整數(shù),因?yàn)椴煌瑢?duì)象的地址值不同,所以這里不存在與p2相同hashCode值的對(duì)象,所以直接存入集合。
- 當(dāng)執(zhí)行set.add(p1)時(shí)(3),時(shí),因?yàn)閜1已經(jīng)存入集合,同一對(duì)象返回的hashCode值是一樣的,繼續(xù)判斷equals是否返回true,因?yàn)槭峭粚?duì)象所以返回true。此時(shí)jdk認(rèn)為該對(duì)象已經(jīng)存在于集合中,所以舍棄。
3.2、測(cè)試二
覆蓋hashCode(),但不覆蓋equals(),仍然會(huì)導(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)時(shí)(1),集合為空,直接存入集合;
- 當(dāng)執(zhí)行set.add(p2)時(shí)(2),首先判斷該對(duì)象p2的hashCode值所在的存儲(chǔ)區(qū)域是否有相同的hashCode,這里覆蓋了hashCode方法,p1和p2的hashCode相等,所以繼續(xù)判斷equals()是否相等,因?yàn)檫@里沒有覆蓋equals(),默認(rèn)使用 “” 來判斷,而 “” 比較的是兩個(gè)對(duì)象的內(nèi)存地址,所以這里equals()會(huì)返回false,所以集合認(rèn)為是不同的對(duì)象,所以將p2存入集合。
- 當(dāng)執(zhí)行set.add(p1)時(shí)(3),時(shí),因?yàn)閜1已經(jīng)存入集合,同一對(duì)象返回的hashCode值是一樣的,并且equals返回true。此時(shí)認(rèn)為該對(duì)象已經(jīng)存在于集合中,所以舍棄。
綜合上述兩個(gè)測(cè)試,要想保證元素的唯一性,必須同時(shí)覆蓋hashCode和equals才行。
(注意:在HashSet中插入同一個(gè)元素(hashCode和equals均相等)時(shí),新加入的元素會(huì)被舍棄,而在HashMap中插入同一個(gè)Key(Value 不同)時(shí),原來的元素會(huì)被覆蓋。)
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方法中,如果兩個(gè)RectObject對(duì)象的x,y值相等的話他們的hashCode值是相等的,同時(shí)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 } }
運(yùn)行結(jié)果:
刪除前的大小size:3
刪除后的大小size:3
在這里,我們發(fā)現(xiàn)了一個(gè)問題,當(dāng)我們調(diào)用了remove刪除r3對(duì)象,以為刪除了r3,但事實(shí)上并沒有刪除,這就叫做內(nèi)存泄露,就是不用的對(duì)象但是他還在內(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方法的時(shí)候,會(huì)先使用對(duì)象的hashCode值去找到這個(gè)對(duì)象,然后進(jìn)行刪除,這種問題就是因?yàn)槲覀冊(cè)谛薷牧?r3 對(duì)象的 y 屬性的值,又因?yàn)镽ectObject對(duì)象的hashCode()方法中有y值參與運(yùn)算,所以r3對(duì)象的hashCode就發(fā)生改變了,所以remove方法中并沒有找到 r3,所以刪除失敗。即 r3的hashCode變了,但是他存儲(chǔ)的位置沒有更新,仍然在原來的位置上,所以當(dāng)我們用他的新的hashCode去找肯定是找不到了.
上面的這個(gè)內(nèi)存泄露告訴我一個(gè)信息:如果我們將對(duì)象的屬性值參與了hashCode的運(yùn)算中,在進(jìn)行刪除的時(shí)候,就不能對(duì)其屬性值進(jìn)行修改,否則會(huì)導(dǎo)致內(nèi)存泄露問題。
5、基本數(shù)據(jù)類型和String類型的hashCode()方法和equals()方法
(1)hashCode():八種基本類型的hashCode()很簡(jiǎn)單就是直接返回他們的數(shù)值大小,String對(duì)象是通過一個(gè)復(fù)雜的計(jì)算方式,但是這種計(jì)算方式能夠保證,如果這個(gè)字符串的值相等的話,他們的hashCode就是相等的。
(2)equals():8種基本類型的equals方法就是直接比較數(shù)值,String類型的equals方法是比較字符串的值的。
到此這篇關(guān)于Java hashCode原理以及與equals()區(qū)別聯(lián)系詳解的文章就介紹到這了,更多相關(guān)Java hashCode內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
ElasticSearch啟動(dòng)成功卻無法在瀏覽器訪問問題解決辦法
因工作的需要,要使用elasticsearch,安裝完了,啟動(dòng)也成功了之后發(fā)現(xiàn)了問題,這篇文章主要給大家介紹了關(guān)于ElasticSearch啟動(dòng)成功卻無法在瀏覽器訪問問題的解決辦法,需要的朋友可以參考下2024-10-10java中toString()、String.valueOf()、(String)?強(qiáng)轉(zhuǎn)的區(qū)別
在實(shí)際開發(fā)中,少不了使用這三種方法對(duì)某一個(gè)類型的數(shù)據(jù)進(jìn)行轉(zhuǎn)?String?的操作,本文就來介紹了java中toString()、String.valueOf()、(String)?強(qiáng)轉(zhuǎn)的區(qū)別,感興趣的可以了解一下2024-06-06Eclipse不自動(dòng)編譯java文件的終極解決方法
這篇文章主要介紹了Eclipse不自動(dòng)編譯java文件的終極解決方法,需要的朋友可以參考下2015-12-12Spring 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實(shí)例化JFileChooser對(duì)象報(bào)空指針異常問題的解決辦法
今天小編就為大家分享一篇關(guān)于實(shí)例化JFileChooser對(duì)象報(bào)空指針異常問題的解決辦法,小編覺得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來看看吧2019-02-02