Java中的equals和hashCode方法詳解(一)

Java中的equals方法和hashCode方法是Object中的,因此每一個對象都是有這兩個方法的,有時候咱們須要實現特定需求,可能要重寫這兩個方法,今天就來介紹一些這兩個方法的做用。
java

equals()和hashCode()方法是用來在同一類中作比較用的,尤爲是在容器裏如set存放同一類對象時用來判斷放入的對象是否重複。
算法

這裏咱們首先要明白一個問題:
app

equals()相等的兩個對象,hashcode()必定相等,equals()不相等的兩個對象,卻並不能證實他們的hashcode()不相等。換句話說,equals()方法不相等的兩個對象,hashCode()有可能相等。(個人理解是因爲哈希碼在生成的時候產生衝突形成的)
ide

在這裏hashCode就比如字典裏每一個字的索引,equals()比如比較的是字典裏同一個字下的不一樣詞語。就好像在字典裏查「自」這個字下的兩個詞語「本身」、「自發」,若是用equals()判斷查詢的詞語相等那麼就是同一個詞語,好比equals()比較的兩個詞語都是「本身」,那麼此時hashCode()方法獲得的值也確定相等;若是用equals()方法比較的是「本身」和「自發」這兩個詞語,那麼獲得結果是不想等,可是這兩個詞都屬於「自」這個字下的詞語因此在查索引時相同,即:hashCode()相同。若是用equals()比較的是「本身」和「他們」這兩個詞語的話那麼獲得的結果也是不一樣的,此時hashCode() 獲得也是不一樣的。函數

反過來:hashcode()不等,必定能推出equals()也不等;hashcode()相等,equals()可能相等,也可能不等。在object類中,hashcode()方法是本地方法,返回的是對象的地址值,而object類中的equals()方法比較的也是兩個對象的地址值,若是equals()相等,說明兩個對象地址值也相等,固然hashcode() 也就相等了;性能

同時hash算法對於查找元素提供了很高的效率
測試

若是想查找一個集合中是否包含有某個對象,大概的程序代碼怎樣寫呢?
ui

你一般是逐一取出每一個元素與要查找的對象進行比較,當發現某個元素與要查找的對象進行equals方法比較的結果相等時,則中止繼續查找並返回確定的信息,不然,返回否認的信息,若是一個集合中有不少個元素,好比有一萬個元素,而且沒有包含要查找的對象時,則意味着你的程序須要從集合中取出一萬個元素進行逐一比較才能獲得結論。this

有人發明了一種哈希算法來提升從集合中查找元素的效率,這種方式將集合分紅若干個存儲區域,每一個對象能夠計算出一個哈希碼,能夠將哈希碼分組(使用不一樣的hash函數來計算的),每組分別對應某個存儲區域,根據一個對象的哈希嗎就能夠肯定該對象應該存儲在哪一個區域HashSet就是採用哈希算法存取對象的集合,它內部採用對某個數字n進行取餘(這種的hash函數是最簡單的)的方式對哈希碼進行分組和劃分對象的存儲區域;Object類中定義了一個hashCode()方法來返回每一個Java對象的哈希碼,當從HashSet集合中查找某個對象時,Java系統首先調用對象的hashCode()方法得到該對象的哈希碼錶,而後根據哈希嗎找到相應的存儲區域,最後取得該存儲區域內的每一個元素與該對象進行equals方法比較;這樣就不用遍歷集合中的全部元素就能夠獲得結論,可見,HashSet集合具備很好的對象檢索性能,可是,HashSet集合存儲對象的效率相對要低些,由於向HashSet集合中添加一個對象時,要先計算出對象的哈希碼和根據這個哈希碼肯定對象在集合中的存放位置爲了保證一個類的實例對象能在HashSet正常存儲,要求這個類的兩個實例對象用equals()方法比較的結果相等時,他們的哈希碼也必須相等;也就是說,若是obj1.equals(obj2)的結果爲true,那麼如下表達式的結果也要爲true:
obj1.hashCode() == obj2.hashCode()
.net

換句話說:當咱們重寫一個對象的equals方法,就必須重寫他的hashCode方法,不過不重寫他的hashCode方法的話,Object對象中的hashCode方法始終返回的是一個對象的hash地址,而這個地址是永遠不相等的。因此這時候即便是重寫了equals方法,也不會有特定的效果的,由於hashCode方法若是都不想等的話,就不會調用equals方法進行比較了,因此沒有意義了。

若是一個類的hashCode()方法沒有遵循上述要求,那麼,當這個類的兩個實例對象用equals()方法比較的結果相等時,他們原本應該沒法被同時存儲進set集合中,可是,若是將他們存儲進HashSet集合中時,因爲他們的hashCode()方法的返回值不一樣(Object中的hashCode方法返回值是永遠不一樣的),第二個對象首先按照哈希碼計算可能被放進與第一個對象不一樣的區域中,這樣,它就不可能與第一個對象進行equals方法比較了,也就可能被存儲進HashSet集合中了,Object類中的hashCode()方法不能知足對象被存入到HashSet中的要求,由於它的返回值是經過對象的內存地址推算出來的,同一個對象在程序運行期間的任什麼時候候返回的哈希值都是始終不變的,因此,只要是兩個不一樣的實例對象,即便他們的equals方法比較結果相等,他們默認的hashCode方法的返回值是不一樣的。

下面來看一下一個具體的例子:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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;

下面是測試代碼:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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發現hashCode是相等的,在比較equals方法,由於equals返回false,因此r1和r4不相等,同一r2和r4也是不相等的,r3和r4也是不相等的,因此r4能夠放到set集合中,那麼結果應該是size:4,那爲何會是3呢?

這時候咱們就須要查看HashSet的源碼了,下面是HashSet中的add方法的源碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 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實現的,咱們在點擊HashMap的put方法,源碼以下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* 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形成的內存泄露的問題:看一下代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

擦,發現一個問題了,並且是個大問題呀,咱們調用了remove刪除r3對象,覺得刪除了r3,但事實上並無刪除,這就叫作內存泄露,就是不用的對象可是他還在內存中。因此咱們屢次這樣操做以後,內存就爆了。看一下remove的源碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 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方法的源碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 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方法源碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* 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就發生改變了,因此remove方法中並無找到r3了,因此刪除失敗。即r3的hashCode變了,可是他存儲的位置沒有更新,仍然在原來的位置上,因此當咱們用他的新的hashCode去找確定是找不到了。
其實上面的方法實現很簡單的:以下圖:

很簡單的一個線性的hash表,使用的hash函數是mod,源碼以下:

1
2
3
4
5
6
/**
* 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的運算中,在進行刪除的時候,就不能對其屬性值進行修改,不然會出現嚴重的問題。

其實咱們也能夠看一下8種基本數據類型對應的對象類型和String類型的hashCode方法和equals方法。

其中8中基本類型的hashCode很簡單就是直接返回他們的數值大小,String對象是經過一個複雜的計算方式,可是這種計算方式可以保證,若是這個字符串的值相等的話,他們的hashCode就是相等的。8種基本類型的equals方法就是直接比較數值,String類型的equals方法是比較字符串的值的。

來自:https://www.jb51.net/article/96138.htm

相關文章
相關標籤/搜索