一、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值有關的對象信息,如果非要修改,則必須先從集合中刪除,然後再加入集合。