hashCode與equals的那些事

一、hashcode的作用

首先說明一點:hashCode是爲了提高在散列結構存儲中(HashTable,HashSet,HashMap)查找的效率,在線性表(ArrayList)中沒有作用。因爲在散列存儲結構中,調用equals()方法之前會首先調用hashcode()方法,如果hashcode()方法返回不同的int值,則不會調用equals()方法。而在線性結構中,是不會調用hashcode()方法的。

我們先假定有一萬個數據要放入集合中,如果我們直接放進去,則當我們要查詢該集合是否包含A時,就需要拿A與一萬個數據進行對比,這種做法是非常低效率的。爲什麼我們不可以像書本的目錄一樣,一開始就將該數據的查找範圍縮小到某個區間內呢?因此,哈希算法就誕生了。

我們首先將這個集合分成若干個存儲區域,再對每個對象計算出一個哈希碼,根據哈希碼,將對象分別放在某個對應的存儲區域,這樣一個對象根據它的哈希碼就可以分到對應的存儲區域。當我們在散列存儲結構中查詢某個對象時,先計算出該對象的哈希值,然後按照哈希值,縮小數據的存儲範圍,在該範圍中進行對比,這樣就不必查詢所有的數據。

二、爲什麼說重寫了equals方法建議同時重寫hashcode方法

在Java中任何一個對象都具備equals(Object obj)和hashCode()這兩個方法,因爲他們是在Object類中定義的。在Object對象中,equals(Object obj)方法默認使用'=='來判斷,比較兩個對象在內存中的地址

public boolean equals(Object obj) {
    return (this == obj);
}

hashCode()方法是一個本地方法,在Object類中的默認實現“將該對象的內部地址轉換成一個整數返回”。

public native int hashCode();

所以,默認情況下,java中的equals方法和hashcode方法作用的都是對象的內存地址

然後看一張圖片

從上面的圖中可以看到在散列結構中存儲一個對象時,會先進行hashCode值的比較,然後進行equals的比較

 實際的代碼

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
			   boolean evict) {
	Node<K,V>[] tab; Node<K,V> p; int n, i;
	if ((tab = table) == null || (n = tab.length) == 0)
		n = (tab = resize()).length;
	if ((p = tab[i = (n - 1) & hash]) == null)
		tab[i] = newNode(hash, key, value, null);
	else {
		Node<K,V> e; K k;
		if (p.hash == hash &&
			((k = p.key) == key || (key != null && key.equals(k))))
			e = p;
		else if (p instanceof TreeNode)
			e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
		else {
			for (int binCount = 0; ; ++binCount) {
				if ((e = p.next) == null) {
					p.next = newNode(hash, key, value, null);
					if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
						treeifyBin(tab, hash);
					break;
				}
				if (e.hash == hash &&
					((k = e.key) == key || (key != null && key.equals(k))))
					break;
				p = e;
			}
		}
		if (e != null) { // existing mapping for key
			V oldValue = e.value;
			if (!onlyIfAbsent || oldValue == null)
				e.value = value;
			afterNodeAccess(e);
			return oldValue;
		}
	}
	++modCount;
	if (++size > threshold)
		resize();
	afterNodeInsertion(evict);
	return null;
}

假如我們重寫了equals方法而沒有重寫hashcode方法,則當兩個對象的hashcode值不相同但是equals方法重寫之後相同,則會跳過調用equals方法,插入散列集合中,此時就放入了兩個equals方法相同的兩個對象,但事實上我們不希望存入兩個equals方法比較結果相同的對象,這樣就產生了一個bug。

三、同一個對象,不要在執行期間修改與hashCode值有關的對象信息,否則會導致內存泄露

public class HashTest {

    public static void main(String[] args) {
        Set<Student> studentSet = new HashSet<>();
        Student s1= new Student("100001", "張三");
        Student s2 = new Student("100002", "李四");
        studentSet.add(s1);
        studentSet.add(s2);
        //修改hashcode相關的屬性
        s1.setName("張三丰");
        //移除s1,但是移除不了,因爲hashCode變了,找不到s1對象
        studentSet.remove(s1);
        Iterator<Student> iterator = studentSet.iterator();
        while (iterator.hasNext()) {
            Student next = iterator.next();
            System.out.println(next.toString());
        }
    }

    public static class Student {
        private String id;
        private String name;

        public Student(String id, String name) {
            this.id = id;
            this.name = name;
        }

        public String getId() {
            return id;
        }

        public void setId(String id) {
            this.id = id;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            Student student = (Student) o;
            return Objects.equals(id, student.id) &&
                    Objects.equals(name, student.name);
        }

        @Override
        public int hashCode() {
            return Objects.hash(id, name);
        }

        @Override
        public String toString() {
            return "Student{" +
                    "id='" + id + '\'' +
                    ", name='" + name + '\'' +
                    '}';
        }
    }

}

// 輸出結果:
Student{id='100003', name='張三丰'}
Student{id='100002', name='李四'}

假設s1的hashCode爲1,s2的hashCode爲2,在存儲時通過hashcode值,s1被分配在區間A1中,s2被分配在A2中。這時修改了s2中與計算hashCode有關的信息(id和name),當調用remove(Object obj)時,首先會查找該hashCode值的對象是否在集合中。假設s1修改後的hashCode值爲3(仍存在區間A1中),這時通過hashcode值查找結果爲空,系統認爲該對象不在集合中,所以不會進行刪除操作。然而用戶以爲該對象已經被刪除,導致該對象長時間不能被釋放,造成內存泄露。

解決該問題的辦法是不要在執行期間修改與hashCode值有關的對象信息,如果非要修改,則必須先從集合中刪除,然後再加入集合。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章