hashcode和equals及雜湊演算法理解
因為會設計到很多equal的知識,所以先深入理解一下equals().
1.equals()
Object類中的預設equals()方法和==是沒有區別的,都是判斷兩個物件是否指向同一個引用,記憶體地址是否相同,即是否就是一個物件。而string類和integer等,都需要重寫equals()方法,用來判斷兩個物件的值是否相等,而不是記憶體地址是否相同。所以,如果元素要儲存到HashSet集合中,必須覆蓋equals方法。一般情況下,如果定義的類會產生很多物件,比如人,學生,書,
通常都需要覆蓋equals。建立物件判斷是否相同的依據。
這邊有個細節,當我們建立類的時候,預設繼承object裡面的equal,而集合裡面有方法比如contains(),還有remove(),判斷是否包含某個元素和移除元素,它得底層也是通過equal來判斷的,所以一定要注意根據自己的需求來重新定義equals。其他的集合物件裡面也是這樣,只是資料結構不同,判斷結構稍有差距。
2.hashCode(以hashset為例)
HashSet: 內部資料結構是雜湊表 ,是不同步的。
如何保證該集合的元素唯一性呢?
是通過物件的hashCode和equals方法來完成物件唯一性的。
如果物件的hashCode值不同,那麼不用判斷equals方法,就直接儲存到雜湊表中。
如果物件的hashCode值相同,那麼要再次判斷物件的equals方法是否為true。
如果為true,視為相同元素,不存。如果為false,那麼視為不同元素,就進行儲存。
為什麼要使用hashcode這種方法呢?
Set元素無序,但元素不可重複。要想保證元素不重複,兩個元素是否重複應該依據什麼來判斷呢?用Object.equals方法。
但若每增加一個元素就檢查一次,那麼當元素很多時,後新增到集合中的元素比較的次數就非常多了。也就是說若集合中
已有1000個元素,那麼第1001個元素加入集合時,它就要呼叫1000次equals方法。這顯然會大大降低效率。於是Java採用
了雜湊表的原理。
當Set接收一個元素時根據該物件的記憶體地址算出hashCode ,這樣根據hashcode來將元素放到相應的位置,這也是它為
什麼是無序的原因,但這樣大大提高了hashset的效率,只有當hashcode的值一樣時,才需要呼叫equals()。
所以:如果元素要儲存到HashSet集合中,必須覆蓋hashCode方法和equals方法。一般情況下,如果定義的類會產生很多物件,
比如人,學生,書,通常都需要覆蓋equals,hashCode方法。建立物件判斷是否相同的依據。
首先我們來看第一個例子:建立一個student物件,包含name和age兩個屬性。
public class HashSetTest {
public static void main(String[] args) {
HashSet hs=new HashSet();
hs.add(new Student("wujie1", 21));
hs.add(new Student("wujie2", 22));
hs.add(new Student("wujie3", 23));
hs.add(new Student("wujie14", 24));
hs.add(new Student("wujie1", 21));
Iterator iterator=hs.iterator();
while(iterator.hasNext()){
Student student=(Student)iterator.next();
System.out.println(student.getName()+"..."+student.getAge());
}
}
}
那麼我們知道,set集合物件中元素是唯一的,那按理,第一條和最後一條是重複的,只應該留下一個,那為什麼兩個都留下來了呢?
第一個原因就是euqals方法,我們知道,set通過equal來判斷兩個物件是否相等,而在object中,euqal的作用是和==一樣的,就是判斷兩個物件是否相等而不是相同,就是是否指向同一個引用,顯然,我們New了五個不同的物件,所以在記憶體中他們的地址都是不同的,所以equals判斷是五個不同的物件,當然都存了進來。所以我們得把判斷是否相等得依據封裝到equals()方法中。
第二個原因就是hashcode,我們並沒有重寫hashcode方法,還是用預設的hashcode的方法。
所以我們在student重寫兩個方法
@Override
public int hashCode() {
// System.out.println(this+".......hashCode");
return name.hashCode()+age*27;
// return 100;
}
@Override
public boolean equals(Object obj) {
if(this == obj)
return true;
if(!(obj instanceof Student
throw new ClassCastException("型別錯誤");
// System.out.println(this+"....equals....."+obj);
Student p = (Student)obj;
return this.name.equals(p.name) && this.age == p.age;
}
首先給每個物件算出hash值,如果相等了,在呼叫equals方法。
在參考別人的部落格時(http://blog.csdn.net/jiangwei0910410003/article/details/22739953),還有一個發現很好玩,就自己去測了一下。將equals方法直接返回false,hashcode不變,那按理,新增最後一個s1的時候先判斷hashcode是否相同,因為時同一個物件,所以肯定相同,那之後呼叫equals是返回false,應該新增進去啊?為什麼列印的size是3不是4呢?
public static void main(String[] args) {
HashSet hs=new HashSet();
Student s1=new Student("wujie5", 25);
hs.add(s1);
hs.add(new Student("wujie2", 22));
hs.add(new Student("wujie3", 23));
hs.add(s1);
System.out.println(hs.size());
/*Iterator iterator=hs.iterator();
while(iterator.hasNext()){
Student student=(Student)iterator.next();
System.out.println(student.getName()+"..."+student.getAge());
}*/
}
因為Hashset是基於Hashmap實現的,它的add方法也是基於hashmap的put方法實現的,所以我們來看hashmap的add方法。
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;
}
首先是判斷hashCode是否相等,不相等的話,直接跳過,相等的話,然後再來比較這兩個物件是否相等或者這兩個物件的equals方法,因為是進行的或操作,所以只要有一個成立即可,那這裡我們就可以解釋了,其實上面的那個集合的大小是3,因為最後的一個r1沒有放進去,以為r1==r1返回true的,所以沒有放進去了。所以集合的大小是3,如果我們將hashCode方法設定成始終返回false的話,這個集合就是4了。所以指向同一個引用的物件,只可能被放進去一次。
還有一個很嚴重的問題就是Hashcode造成的記憶體洩漏。看程式碼。
public static void main(String[] args) {
HashSet<Student> hs=new HashSet<Student>();
Student s1=new Student("wujie5", 25);
hs.add(s1);
hs.add(new Student("wujie2", 22));
hs.add(new Student("wujie3", 23));
//hs.add(s1);
s1.setAge(27);
System.out.println("刪除前的大小"+hs.size());
hs.remove(s1);
System.out.println("刪除前的大小"+hs.size());
}
我們remove了一個,那集合的size應該還剩2,但是測試結果size還是3.這就是大問題了,不用的物件結果還在記憶體當中,那這樣時間長了記憶體肯定會滿了。為什麼會這樣呢?以下為remove原始碼。hashset的emove方法同樣是以hashmap的remove方法為基礎的,我們直接看hashmap的remove()。
public V remove(Object key) {
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.value);
}
這邊又出現了一個removeEntryKey().再看。
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的運算中,在進行刪除的時候,就不能對其屬性值進行修改,否則會出現嚴重的問題。