equals()和HashCode()深入理解以及Hash算法原理
1.深入理解equals():
- 在我的一篇博客“==”和.equals()的區別中向讀者提出提醒: Object類中的equals方法和“==”是一樣的,沒有區別,即倆個對象的比較是比較他們的棧內存中存儲的內存地址。而String類,Integer類等等一些類,是重寫了equals方法,才使得equals和“==不同”,他們比較的是值是不是相等。所以,當自己創建類時,自動繼承了Object的equals方法,要想實現不同的等於比較,必須重寫equals方法。
- 我們看下面這個例子:
package cn.galc.test;
public class TestEquals {
public static void main(String[] args) {
/**
* 這裏使用構造方法Cat()在堆內存裏面new出了兩隻貓,
* 這兩隻貓的color,weight,height都是一樣的,
* 但c1和c2卻永遠不會相等,這是因爲c1和c2分別爲堆內存裏面兩隻貓的引用對象,
* 裏面裝着可以找到這兩隻貓的地址,但由於兩隻貓在堆內存裏面存儲在兩個不同的空間裏面,
* 所以c1和c2分別裝着不同的地址,因此c1和c2永遠不會相等。
*/
Cat c1 = new Cat(1, 1, 1);
Cat c2 = new Cat(1, 1, 1);
System.out.println("c1==c2的結果是:"+(c1==c2));//false
System.out.println("c1.equals(c2)的結果是:"+c1.equals(c2));//false
}
}
class Cat {
int color, weight, height;
public Cat(int color, int weight, int height) {
this.color = color;
this.weight = weight;
this.height = height;
}
}
畫出內存分析圖分析c1和c2比較的結果,當執行Cat c1 = new Cat(1,1,1); Cat c2 = new Cat(1,1,1);
之後內存之中佈局如下圖:
- 由此我們看出,當我們new一個對象時,將在內存里加載一份它自己的內存,而不是共用!對於static修飾的變量和方法則保存在方法區中,只加載一次,不會再多copy一份內存。
- 所以我們在判斷倆個對象邏輯上是否相等,即對象的內容是否相等不能直接使用繼承於Object類的equals()方法,我們必須得重寫equals()方法,改變這個方法默認的實現。下面在Cat類裏面重寫這個繼承下來的equals()方法:
class Cat {
int color, weight, height;
public Cat(int color, int weight, int height) {
this.color = color;
this.weight = weight;
this.height = height;
}
/**
* 這裏是重寫相等從Object類繼承下來的equals()方法,改變這個方法默認的實現,
* 通過我們自己定義的實現來判斷決定兩個對象在邏輯上是否相等。
* 這裏我們定義如果兩隻貓的color,weight,height都相同,
* 那麼我們就認爲這兩隻貓在邏輯上是一模一樣的,即這兩隻貓是“相等”的。
*/
public boolean equals(Object obj){
if (obj==null){
return false;
}
else{
/**
* instanceof是對象運算符。
* 對象運算符用來測定一個對象是否屬於某個指定類或指定的子類的實例。
* 如果左邊的對象是右邊的類創建的對象,則運算結果爲true,否則爲false。
*/
if (obj instanceof Cat){
Cat c = (Cat)obj;
if (c.color==this.color && c.weight==this.weight && c.height==this.height){
return true;
}
}
}
return false;
}
}
- 設計思路很簡單:先判斷比較對象是否爲null—>判斷比較對象是否爲要比較類的實例—–>比較倆個成員變量是否完全相等。
//另外一種常用重寫方法
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
People other = (People) obj;
if (age != other.age) return false;
if (firstName == null) {
if (other.firstName != null) return false;
} else if (!firstName.equals(other.firstName)) return false;
if (lastName == null) {
if (other.lastName != null) return false;
} else if (!lastName.equals(other.lastName)) return false;
return true;
}
- 這樣通過在類中重寫equals()方法,我們可以比較在同一個類下不同對象是否相等了。
2.Hash算法原理以及HashCode深入理解
- Java中的Collection有兩類,一類是List,一類是Set。List內的元素是有序的,元素可以重複。Set元素無序,但元素不可重複。要想保證元素不重複,兩個元素是否重複應該依據什麼來判斷呢?用Object.equals方法。但若每增加一個元素就檢查一次,那麼當元素很多時,後添加到集合中的元素比較的次數就非常多了。也就是說若集合中已有1000個元素,那麼第1001個元素加入集合時,它就要調用1000次equals方法。這顯然會大大降低效率。於是Java採用了哈希表的原理。
- 當Set接收一個元素時根據該對象的內存地址算出hashCode,看它屬於哪一個區間,再這個區間裏調用equeals方法。【特別注意】這裏需要注意的是:當倆個對象的hashCode值相同的時候,Hashset會將對象保存在同一個位置,但是他們equals返回false,所以實際上這個位置採用鏈式結構來保存多個對象。
上面方法確實提高了效率。但一個面臨問題:若兩個對象equals相等,但不在一個區間,因爲hashCode的值在重寫之前是對內存地址計算得出,所以根本沒有機會進行比較,會被認爲是不同的對象。所以Java對於eqauls方法和hashCode方法是這樣規定的:
1 如果兩個對象相同,那麼它們的hashCode值一定要相同。也告訴我們重寫equals方法,一定要重寫hashCode方法,也就是說hashCode值要和類中的成員變量掛上鉤,對象相同–>成員變量相同—->hashCode值一定相同。
2 如果兩個對象的hashCode相同,它們並不一定相同,這裏的對象相同指的是用eqauls方法比較。接下來內容就是轉載自:http://blog.csdn.net/jiangwei0910410003/article/details/22739953博客*********************************************************************
下面來看一下一個具體的例子: 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發現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實現的,我們在點擊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
擦,發現一個問題了,而且是個大問題呀,我們調用了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就發生改變了,所以remove方法中並沒有找到r3了,所以刪除失敗。即r3的hashCode變了,但是他存儲的位置沒有更新,仍然在原來的位置上,所以當我們用他的新的hashCode去找肯定是找不到了。
上面的這個內存泄露告訴我一個信息:如果我們將對象的屬性值參與了hashCode的運算中,在進行刪除的時候,就不能對其屬性值進行修改,否則會出現嚴重的問題。