1. 程式人生 > >hashcode和equals及雜湊演算法理解

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的運算中,在進行刪除的時候,就不能對其屬性值進行修改,否則會出現嚴重的問題。