首先之所以會將hashCode()與equals()放到一起是因爲它們具備一個相同的作用:用來比較某個東西。其中hashCode()主要是用在hash表中提高 查找效率,而equals()則相對而言使用更廣泛,用於比較兩個對象的值是否相同,在Java集合框架中它們共同出現用來比較某元素是否相等。
一hashCode()
hashCode()位於Object類中,其定義如下:
public native int hashCode();
從上述的定義可以看到hashCode()屬於本地方法。
1爲何需要hashCode()或者說hashCode()的作用:
我們知道當判斷兩個對象是否相同時我們時我們可以使用equals()方法,那麼爲何需要hashCode()呢?其實從名字上就可以看出hashCode的作用是爲hahs表準備的,是爲了提高查找效率而存在的,如在java集合框架中的HashSet,HashMap。
那麼當我們往集合中添加一個元素的時候如何判斷該元素是否存在呢?(注意java集合框架中除了ArrayList與LinkedList外都不允許存在重複元素,對於Map集合重複指的是K/V都相同)如果不用hashCode(),那麼你可能會想到用equals()方法將集合中已存在的元素與待插入元素一個一個比較,但是這樣做顯然效率低下,因此hashCode()應運而生,hashCode()就是爲了解決元素是否重複而存在的,它採用一定的規則將和對象相關的信息(比如對象的存儲地址,對象的字段等)映射成一個數值,這個數值稱作爲散列值。它用來標識一個對象,如果hash值不同則表示這兩個對象一定不同,如果hash值相同,則這兩個對象可能相同也可能不同,此時需要使用equals()來判斷這兩個對象是否相同注意:equals()纔是用來判斷兩個對象是否相同的核心,hashCode()只是爲了提高在集合中的查找效率而存在的,只要hashCode值不同則這兩個對象一定不同,如在HashMap的put函數中,通過hash的值來判斷是否存在該元素,如果hash值不存在(tab[i]==null),則一定不存在該元素,若hash值存在,則可能存在該元素,需要通過equals方法來確定,如果hash值存在且key.equals.(k)則表明存在該元素,直接更新其值,否則表明不存在,則採用鏈表或紅黑樹的方式將元素添加到tab[i]對應的鏈表或紅黑樹中,這樣的話就能大大減少使用equals的次數,從而提高效率。HashMap的put函數源碼如下:
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)//1
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)//2首先判斷tab[(n - 1) & hash]處是否爲空,如果是代表該數組下標爲[(n - 1) & hash]的位置無元素,可直接put
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))))//如果Hash值相同,則調用equals方法來確定是否存在該元素,則執行break語句
break;//跳出for循環,執行下面的if語句,即<span style="font-family: Arial, Helvetica, sans-serif;">existing mapping for key,則更新value的值,e.value=value。</span>
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;
}
可以看到先通過hash得到插入的數組索引i,如果tab[i]==null,表示此下標處無元素存在,可直接添加元素,否則出現衝突,掃描鏈表或紅黑樹,在此過程中通過equals方法來確定是否存在該元素,如果存在,則直接更新,否則採用鏈表或紅黑樹的方式將元素添加到tab[i]對應的鏈表或紅黑樹中。即通過hash的值來判斷是否存在該元素,如果hash值不存在(tab[i]==null),則一定不存在該元素,若hash值存在,則可能存在該元素,需要通過equals方法來確定,如果hash值存在且key.equals.(k)則表明存在該元素,直接更新其值,否則表明不存在,則採用鏈表或紅黑樹的方式將元素添加到tab[i]對應的鏈表或紅黑樹中
那麼我們能否僅僅根據hashCode()的值來判斷兩個對象是否相等呢?答案是不能,因爲不同的對象可能會生成相同的hashcode值。雖然不能根據hashcode值判斷兩個對象是否相等,但是可以直接根據hashcode值判斷兩個對象不等,如果兩個對象的hashcode值不等,則必定是兩個不同的對象。如果要判斷兩個對象是否真正相等,必須通過equals方法。
也就是說對於兩個對象,如果調用equals方法得到的結果爲true,則兩個對象的hashcode值必定相等;
如果equals方法得到的結果爲false,則兩個對象的hashcode值不一定不同;
如果兩個對象的hashcode值不等,則equals方法得到的結果必定爲false;
如果兩個對象的hashcode值相等,則equals方法得到的結果未知。
二equals()
equals()同樣也是位於Object類中的一個方法,我們來看一下其源碼:
public boolean equals(Object obj) {
return (this == obj);
}
可以看到,Object類中的equals方法是用來判斷兩個對象的地址是否相等(this==obj),但是我們知道在使用String類的時候equals比較的是兩個字符串的內容是怎麼回事呢?這時因爲String重寫了Object類的equals(),因爲對於字符串而言我們更關心其內容,而不是其對象地址,,我們來看一下String類中的equals()源碼:
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
我們可以看到,在String的equals()中首先判斷這兩個對象是否相同,若相同則直接返回true(因爲同一個地址的String其存儲的內容一定相同),若這兩個對象不同則首先獲取這兩個字符串的長度,如果長度相同則逐個比較每個字符是否相同,若相同則返回true,如果長度不同則直接返回false。所以String類中的equals()比較的是兩個字符串對象的內容。其他的一些類諸如Double,Date,Integer等,都對equals方法進行了重寫用來比較指向的對象所存儲的內容是否相等。
1)對於==,如果作用於基本數據類型的變量,則直接比較其存儲的 “值”是否相等;
如果作用於引用類型的變量,則比較的是所指向的對象的地址
2)對於equals方法,注意:equals方法不能作用於基本數據類型的變量
如果沒有對equals方法進行重寫,則比較的是引用類型的變量所指向的對象的地址;
諸如String、Date等類對equals方法進行了重寫的話,比較的是所指向的對象的內容。
三重寫equals()方法與hashCode()方法
我們知道java中的String,Date,Integer等類重寫了equals()方法,那麼什麼時候需要重寫equals方法呢?當一個類需要定義屬於自己的“邏輯相等”的概念而不僅僅是對象引用是否相等時則需要重寫該方法,那麼什麼時候需要重寫hashCode()方法呢?其實hashCode()方法的重寫不是強制的,它是爲了在將你自己定義的類存入java集合框架然後get出來時確保是同一個對象所做的一種規範性要求,以滿足java規範中的相等的對象必須具有相等的散列碼,如下面的例子:
class People{
private String name;
private int age;
public People(String name,int age) {
this.name = name;
this.age = age;
}
public void setAge(int age){
this.age = age;
}
@Override
public boolean equals(Object obj) {
// TODO Auto-generated method stub
return this.name.equals(((People)obj).name) && this.age== ((People)obj).age;
}
}
public class Main {
public static void main(String[] args) {
People p1 = new People("Jack", 12);
System.out.println(p1.hashCode());
HashMap<People, Integer> hashMap = new HashMap<People, Integer>();
hashMap.put(p1, 1);
System.out.println(hashMap.get(new People("Jack", 12)));
}
}
在這裏我只重寫了equals方法,也就說如果兩個People對象,如果它的姓名和年齡相等,則認爲是同一個人。這段代碼本來的意願是想這段代碼輸出結果爲“1”,但是事實上它輸出的是“null”。爲什麼呢?原因就在於重寫equals方法的同時忘記重寫hashCode方法。
雖然通過重寫equals方法使得邏輯上姓名和年齡相同的兩個對象被判定爲相等的對象(跟String類類似),但是要知道默認情況下,hashCode方法是將對象的存儲地址進行映射。而java集合get方法時首先會通過hashCode()得到的hash值來判斷是否存在該元素,如果hash值不存在,則直接返回null,因此上述代碼輸出結果爲null
HashMap中get(k)源碼如下:
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) { //通過該hash值與table的長度n-1相與得到數組的索引first
if (first.hash == hash && // always check first node//如果hash值不同下面的代碼將不會執行
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)//代表該HashMap爲數組+紅黑樹結構
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {//否則代表是數組+鏈表結構
if (e.hash == hash && <span style="font-family: Arial, Helvetica, sans-serif;">//如果hash值不同下面的代碼將不會執行</span>
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;//<span style="font-family: Arial, Helvetica, sans-serif;">如果上述紅黑樹與鏈表結構中都不存在該hash值則表示該HashMap中不存在該元素,返回null</span>
}
因此如果想上述代碼輸出結果爲“1”,很簡單,只需要重寫hashCode方法,讓equals方法和hashCode方法始終在邏輯上保持一致性。
class People{
private String name;
private int age;
public People(String name,int age) {
this.name = name;
this.age = age;
}
public void setAge(int age){
this.age = age;
}
@Override
public int hashCode() {
// TODO Auto-generated method stub
return name.hashCode()*37+age;
}
@Override
public boolean equals(Object obj) {
// TODO Auto-generated method stub
return this.name.equals(((People)obj).name) && this.age== ((People)obj).age;
}
}
public class Main {
public static void main(String[] args) {
People p1 = new People("Jack", 12);
System.out.println(p1.hashCode());
HashMap<People, Integer> hashMap = new HashMap<People, Integer>();
hashMap.put(p1, 1);
System.out.println(hashMap.get(new People("Jack", 12)));
}
}
這樣輸出結果即爲1與預期結果相同。四總結:
1hashCode()與equals()都屬於Object類中的方法,因此java中的所有的類中都默認存在這兩種方法。
2爲滿足java規範中的相等的對象必須具有相等的hash值,通常在重寫equals()時一定要重寫hashCode()方法。
3如果沒有對equals方法進行重寫,則比較的是引用類型的變量所指向的對象的地址;對於==,如果作用於基本數據類型的變量,則直接比較其存儲的 “值”是否相等;
如果作用於引用類型的變量,則比較的是所指向的對象的地址。