Java中的equals方法和hashCode方法是Object中的奖磁,所以每個對象都是有這兩個方法的产舞,有時候我們需要實現(xiàn)特定需求式撼,可能要重寫這兩個方法宪潮,今天就來介紹一些這兩個方法的作用似枕。
equals()和hashCode()方法是用來在同一類中做比較用的盖淡,尤其是在容器里如set存放同一類對象時用來判斷放入的對象是否重復。
這里我們首先要明白一個問題:
equals()相等的兩個對象菠净,hashcode()一定相等禁舷,equals()不相等的兩個對象,卻并不能證明他們的hashcode()不相等毅往。換句話說牵咙,equals()方法不相等的兩個對象,hashCode()有可能相等攀唯。(我的理解是由于哈希碼在生成的時候產(chǎn)生沖突造成的)
在這里hashCode就好比字典里每個字的索引洁桌,equals()好比比較的是字典里同一個字下的不同詞語。就好像在字典里查“自”這個字下的兩個詞語“自己”侯嘀、“自發(fā)”另凌,如果用equals()判斷查詢的詞語相等那么就是同一個詞語谱轨,比如equals()比較的兩個詞語都是“自己”,那么此時hashCode()方法得到的值也肯定相等吠谢;如果用equals()方法比較的是“自己”和“自發(fā)”這兩個詞語土童,那么得到結果是不想等,但是這兩個詞都屬于“自”這個字下的詞語所以在查索引時相同工坊,即:hashCode()相同献汗。如果用equals()比較的是“自己”和“他們”這兩個詞語的話那么得到的結果也是不同的,此時hashCode() 得到也是不同的王污。
反過來:hashcode()不等罢吃,一定能推出equals()也不等;hashcode()相等昭齐,equals()可能相等尿招,也可能不等。在object類中阱驾,hashcode()方法是本地方法就谜,返回的是對象的地址值,而object類中的equals()方法比較的也是兩個對象的地址值啊易,如果equals()相等吁伺,說明兩個對象地址值也相等,當然hashcode() 也就相等了租谈;
同時hash算法對于查找元素提供了很高的效率
如果想查找一個集合中是否包含有某個對象篮奄,大概的程序代碼怎樣寫呢?
你通常是逐一取出每個元素與要查找的對象進行比較割去,當發(fā)現(xiàn)某個元素與要查找的對象進行equals方法比較的結果相等時窟却,則停止繼續(xù)查找并返回肯定的信息,否則呻逆,返回否定的信息夸赫,如果一個集合中有很多個元素,比如有一萬個元素咖城,并且沒有包含要查找的對象時茬腿,則意味著你的程序需要從集合中取出一萬個元素進行逐一比較才能得到結論。
有人發(fā)明了一種哈希算法來提高從集合中查找元素的效率宜雀,這種方式將集合分成若干個存儲區(qū)域切平,每個對象可以計算出一個哈希碼,可以將哈希碼分組(使用不同的hash函數(shù)來計算的)辐董,每組分別對應某個存儲區(qū)域悴品,根據(jù)一個對象的哈希嗎就可以確定該對象應該存儲在哪個區(qū)域HashSet就是采用哈希算法存取對象的集合,它內部采用對某個數(shù)字n進行取余(這種的hash函數(shù)是最簡單的)的方式對哈希碼進行分組和劃分對象的存儲區(qū)域;Object類中定義了一個hashCode()方法來返回每個Java對象的哈希碼苔严,當從HashSet集合中查找某個對象時定枷,Java系統(tǒng)首先調用對象的hashCode()方法獲得該對象的哈希碼表,然后根據(jù)哈希嗎找到相應的存儲區(qū)域届氢,最后取得該存儲區(qū)域內的每個元素與該對象進行equals方法比較欠窒;這樣就不用遍歷集合中的所有元素就可以得到結論,可見悼沈,HashSet集合具有很好的對象檢索性能贱迟,但是,HashSet集合存儲對象的效率相對要低些絮供,因為向HashSet集合中添加一個對象時,要先計算出對象的哈希碼和根據(jù)這個哈希碼確定對象在集合中的存放位置為了保證一個類的實例對象能在HashSet正常存儲茶敏,要求這個類的兩個實例對象用equals()方法比較的結果相等時壤靶,他們的哈希碼也必須相等;也就是說惊搏,如果obj1.equals(obj2)的結果為true,那么以下表達式的結果也要為true:
obj1.hashCode() == obj2.hashCode()
換句話說:當我們重寫一個對象的equals方法贮乳,就必須重寫他的hashCode方法,不過不重寫他的hashCode方法的話恬惯,Object對象中的hashCode方法始終返回的是一個對象的hash地址向拆,而這個地址是永遠不相等的。所以這時候即使是重寫了equals方法酪耳,也不會有特定的效果的浓恳,因為hashCode方法如果都不想等的話,就不會調用equals方法進行比較了碗暗,所以沒有意義了颈将。
如果一個類的hashCode()方法沒有遵循上述要求,那么言疗,當這個類的兩個實例對象用equals()方法比較的結果相等時晴圾,他們本來應該無法被同時存儲進set集合中,但是噪奄,如果將他們存儲進HashSet集合中時死姚,由于他們的hashCode()方法的返回值不同(Object中的hashCode方法返回值是永遠不同的),第二個對象首先按照哈希碼計算可能被放進與第一個對象不同的區(qū)域中勤篮,這樣都毒,它就不可能與第一個對象進行equals方法比較了,也就可能被存儲進HashSet集合中了叙谨,Object類中的hashCode()方法不能滿足對象被存入到HashSet中的要求温鸽,因為它的返回值是通過對象的內存地址推算出來的,同一個對象在程序運行期間的任何時候返回的哈希值都是始終不變的,所以涤垫,只要是兩個不同的實例對象姑尺,即使他們的equals方法比較結果相等,他們默認的hashCode方法的返回值是不同的蝠猬。
下面來看一下一個具體的例子:
RectObject對象:
package com.weijia.demo;
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;
下面是測試代碼
package com.weijia.demo;
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);
set.add(r1);
System.out.println("size:"+set.size());
}
}
我們向HashSet中存入到了四個對象柄粹,打印set集合的大小,結果是多少呢匆绣?
運行結果:size:2
為什么會是2呢驻右?這個很簡單了吧,因為我們重寫了RectObject類的hashCode方法崎淳,只要RectObject對象的x,y屬性值相等那么他的hashCode值也是相等的堪夭,所以先比較hashCode的值,r1和r2對象的x,y屬性值不等拣凹,所以他們的hashCode不相同的森爽,所以r2對象可以放進去,但是r3對象的x,y屬性值和r1對象的屬性值相同的嚣镜,所以hashCode是相等的爬迟,這時候在比較r1和r3的equals方法,因為他么兩的x,y值是相等的菊匿,所以r1,r3對象是相等的付呕,所以r3不能放進去了,同樣最后再添加一個r1也是沒有沒有添加進去的捧请,所以set集合中只有一個r1和r2這兩個對象
下面我們把RectObject對象中的hashCode方法注釋凡涩,即不重寫Object對象中的hashCode方法,在運行一下代碼:
運行結果:size:3
這個結果也是很簡單的疹蛉,首先判斷r1對象和r2對象的hashCode活箕,因為Object中的hashCode方法返回的是對象本地內存地址的換算結果,不同的實例對象的hashCode是不相同的可款,同樣因為r3和r1的hashCode也是不相等的育韩,但是r1==r1的,所以最后set集合中只有r1,r2,r3這三個對象闺鲸,所以大小是3
下面我們把RectObject對象中的equals方法中的內容注釋筋讨,直接返回false,不注釋hashCode方法摸恍,運行一下代碼:
運行結果:size:3
這個結果就有點意外了悉罕,我們來分析一下:
首先r1和r2的對象比較hashCode赤屋,不相等,所以r2放進set中壁袄,再來看一下r3,比較r1和r3的hashCode方法类早,是相等的,然后比較他們兩的equals方法嗜逻,因為equals方法始終返回false,所以r1和r3也是不相等的涩僻,r3和r2就不用說了,他們兩的hashCode是不相等的栈顷,所以r3放進set中逆日,再看r4,比較r1和r4發(fā)現(xiàn)hashCode是相等的,在比較equals方法萄凤,因為equals返回false,所以r1和r4不相等室抽,同一r2和r4也是不相等的,r3和r4也是不相等的蛙卤,所以r4可以放到set集合中狠半,那么結果應該是size:4,那為什么會是3呢?
這時候我們就需要查看HashSet的源碼了颤难,下面是HashSet中的add方法的源碼:
/**
* Adds the specified element to this set if it is not already present.
* More formally, adds the specified element <tt>e</tt> to this set if
* this set contains no element <tt>e2</tt> such that
* <tt>(e==null ? e2==null : e.equals(e2))</tt>.
* If this set already contains the element, the call leaves the set
* unchanged and returns <tt>false</tt>.
*
* @param e element to be added to this set
* @return <tt>true</tt> if this set did not already contain the specified
* element
*/
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
這里我們可以看到其實HashSet是基于HashMap實現(xiàn)的,我們在點擊HashMap的put方法已维,源碼如下:
/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*/
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key);
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;
}
我們主要來看一下if的判斷條件行嗤,
首先是判斷hashCode是否相等,不相等的話垛耳,直接跳過栅屏,相等的話,然后再來比較這兩個對象是否相等或者這兩個對象的equals方法堂鲜,因為是進行的或操作栈雳,所以只要有一個成立即可,那這里我們就可以解釋了缔莲,其實上面的那個集合的大小是3,因為最后的一個r1沒有放進去哥纫,以為r1==r1返回true的,所以沒有放進去了痴奏。所以集合的大小是3蛀骇,如果我們將hashCode方法設置成始終返回false的話,這個集合就是4了读拆。
最后我們在來看一下hashCode造成的內存泄露的問題:看一下代碼:
package com.weijia.demo;
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());
set.remove(r3);
System.out.println("刪除后的大小size:"+set.size());
}
}
運行結果:
刪除前的大小size:3
刪除后的大小size:3
發(fā)現(xiàn)一個問題了擅憔,而且是個大問題呀,我們調用了remove刪除r3對象檐晕,以為刪除了r3,但事實上并沒有刪除暑诸,這就叫做內存泄露,就是不用的對象但是他還在內存中。所以我們多次這樣操作之后个榕,內存就爆了篡石。看一下remove的源碼:
/**
* Removes the specified element from this set if it is present.
* More formally, removes an element <tt>e</tt> such that
* <tt>(o==null ? e==null : o.equals(e))</tt>,
* if this set contains such an element. Returns <tt>true</tt> if
* this set contained the element (or equivalently, if this set
* changed as a result of the call). (This set will not contain the
* element once the call returns.)
*
* @param o object to be removed from this set, if present
* @return <tt>true</tt> if the set contained the specified element
*/
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
然后再看一下remove方法的源碼:
/**
* Removes the mapping for the specified key from this map if present.
*
* @param key key whose mapping is to be removed from the map
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*/
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;
}
我們看到笛洛,在調用remove方法的時候夏志,會先使用對象的hashCode值去找到這個對象,然后進行刪除苛让,這種問題就是因為我們在修改了r3對象的y屬性的值沟蔑,又因為RectObject對象的hashCode方法中有y值參與運算,所以r3對象的hashCode就發(fā)生改變了,所以remove方法中并沒有找到r3了狱杰,所以刪除失敗瘦材。即r3的hashCode變了,但是他存儲的位置沒有更新仿畸,仍然在原來的位置上食棕,所以當我們用他的新的hashCode去找肯定是找不到了。
其實上面的方法實現(xiàn)很簡單的:如下圖:
很簡單的一個線性的hash表错沽,使用的hash函數(shù)是mod,源碼如下:
/**
* Returns index for hash code h.
*/
static int indexFor(int h, int length) {
return h & (length-1);
}
這個其實就是mod運算簿晓,只是這種運算比%運算要高效。
1,2,3,4,5表示是mod的結果千埃,每個元素對應的是一個鏈表結構憔儿,所以說想刪除一個Entry<K,V>的話,首先得到hashCode放可,從而獲取到鏈表的頭結點谒臼,然后再遍歷這個鏈表,如果hashCode和equals相等就刪除這個元素耀里。
上面的這個內存泄露告訴我一個信息:如果我們將對象的屬性值參與了hashCode的運算中蜈缤,在進行刪除的時候,就不能對其屬性值進行修改冯挎,否則會出現(xiàn)嚴重的問題底哥。
其實我們也可以看一下8種基本數(shù)據(jù)類型對應的對象類型和String類型的hashCode方法和equals方法。
其中8中基本類型的hashCode很簡單就是直接返回他們的數(shù)值大小织堂,String對象是通過一個復雜的計算方式叠艳,但是這種計算方式能夠保證,如果這個字符串的值相等的話易阳,他們的hashCode就是相等的附较。8種基本類型的equals方法就是直接比較數(shù)值,String類型的equals方法是比較字符串的值的潦俺。