java基礎(十六)----- equals()與hashCode()方法詳解 —— 面試必問


本文將詳解 equals()與hashCode()方法

概述

java.lang.Object類中有兩個非常重要的方法:

public boolean equals(Object obj)
public int hashCode()

Object類是類繼承結構的基礎,所以是每一個類的父類。所有的對象,包括數組,都實現了在Object類中定義的方法。

equals()方法詳解

equals()方法是用來判斷其他的對象是否和該對象相等.

equals()方法在object類中定義如下:

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

很明顯是對兩個對象的地址值進行的比較(即比較引用是否相同)。但是我們知道,String 、Math、Integer、Double等這些封裝類在使用equals()方法時,已經覆蓋了object類的equals()方法

比如在String類中如下:

public boolean equals(Object anObject) {  
    if (this == anObject) {  
        return true;  
    }  
    if (anObject instanceof String) {  
        String anotherString = (String)anObject;  
        int n = count;  
        if (n == anotherString.count) {  
            char v1[] = value;  
            char v2[] = anotherString.value;  
            int i = offset;  
            int j = anotherString.offset;  
            while (n– != 0) {  
                if (v1[i++] != v2[j++])  
                    return false;  
            }  
            return true;  
        }  
    }  
    return false;  
} 

很明顯,這是進行的內容比較,而已經不再是地址的比較。依次類推Math、Integer、Double等這些類都是重寫了equals()方法的,從而進行的是內容的比較。當然,基本類型是進行值的比較。

需要注意的是當equals()方法被override時,hashCode()也要被override。按照一般hashCode()方法的實現來說,相等的對象,它們的hash code一定相等。

hashcode() 方法詳解

hashCode()方法給對象返回一個hash code值。這個方法被用於hash tables,例如HashMap。

它的性質是:

  • 在一個Java應用的執行期間,如果一個對象提供給equals做比較的信息沒有被修改的話,該對象多次調用hashCode()方法,該方法必須始終如一返回同一個integer。

  • 如果兩個對象根據equals(Object)方法是相等的,那么調用二者各自的hashCode()方法必須產生同一個integer結果。

  • 並不要求根據equals(java.lang.Object)方法不相等的兩個對象,調用二者各自的hashCode()方法必須產生不同的integer結果。然而,程序員應該意識到對於不同的對象產生不同的integer結果,有可能會提高hash table的性能。

大量的實踐表明,由Object類定義的hashCode()方法對於不同的對象返回不同的integer。

在object類中,hashCode定義如下:

public native int hashCode();

說明是一個本地方法,它的實現是根據本地機器相關的。當然我們可以在自己寫的類中覆蓋hashcode()方法,比如String、Integer、Double等這些類都是覆蓋了hashcode()方法的。例如在String類中定義的hashcode()方法如下:

public int hashCode() {  
    int h = hash;  
    if (h == 0) {  
        int off = offset;  
        char val[] = value;  
        int len = count;  
        for (int i = 0; i < len; i++) {  
            h = 31 * h + val[off++];  
        }  
        hash = h;  
    }  
    return h;  
}  

想要弄明白hashCode的作用,必須要先知道Java中的集合。  
總的來說,Java中的集合(Collection)有兩類,一類是List,再有一類是Set。前者集合內的元素是有序的,元素可以重復;后者元素無序,但元素不可重復。這里就引出一個問題:要想保證元素不重復,可兩個元素是否重復應該依據什么來判斷呢?
這就是Object.equals方法了。但是,如果每增加一個元素就檢查一次,那么當元素很多時,后添加到集合中的元素比較的次數就非常多了。也就是說,如果集合中現在已經有1000個元素,那么第1001個元素加入集合時,它就要調用1000次equals方法。這顯然會大大降低效率。   
      於是,Java采用了哈希表的原理。哈希(Hash)實際上是個人名,由於他提出一哈希算法的概念,所以就以他的名字命名了。哈希算法也稱為散列算法,是將數據依特定算法直接指定到一個地址上,初學者可以簡單理解,hashCode方法實際上返回的就是對象存儲的物理地址(實際可能並不是)。  
      這樣一來,當集合要添加新的元素時,先調用這個元素的hashCode方法,就一下子能定位到它應該放置的物理位置上。如果這個位置上沒有元素,它就可以直接存儲在這個位置上,不用再進行任何比較了;如果這個位置上已經有元素了,就調用它的equals方法與新元素進行比較,相同的話就不存了,不相同就散列其它的地址。所以這里存在一個沖突解決的問題。這樣一來實際調用equals方法的次數就大大降低了,幾乎只需要一兩次。

簡而言之,在集合查找時,hashcode能大大降低對象比較次數,提高查找效率!

Java對象的eqauls方法和hashCode方法是這樣規定的:

1、相等(相同)的對象必須具有相等的哈希碼(或者散列碼)。

2、如果兩個對象的hashCode相同,它們並不一定相同。

關於第一點,相等(相同)的對象必須具有相等的哈希碼(或者散列碼),為什么?

想象一下,假如兩個Java對象A和B,A和B相等(eqauls結果為true),但A和B的哈希碼不同,則A和B存入HashMap時的哈希碼計算得到的HashMap內部數組位置索引可能不同,那么A和B很有可能允許同時存入HashMap,顯然相等/相同的元素是不允許同時存入HashMap,HashMap不允許存放重復元素。

關於第二點,兩個對象的hashCode相同,它們並不一定相同

也就是說,不同對象的hashCode可能相同;假如兩個Java對象A和B,A和B不相等(eqauls結果為false),但A和B的哈希碼相等,將A和B都存入HashMap時會發生哈希沖突,也就是A和B存放在HashMap內部數組的位置索引相同這時HashMap會在該位置建立一個鏈接表,將A和B串起來放在該位置,顯然,該情況不違反HashMap的使用原則,是允許的。當然,哈希沖突越少越好,盡量采用好的哈希算法以避免哈希沖突。

所以,Java對於eqauls方法和hashCode方法是這樣規定的:

1.如果兩個對象相同,那么它們的hashCode值一定要相同;
2.如果兩個對象的hashCode相同,它們並不一定相同(這里說的對象相同指的是用eqauls方法比較)。  
如不按要求去做了,會發現相同的對象可以出現在Set集合中,同時,增加新元素的效率會大大下降。
3.equals()相等的兩個對象,hashcode()一定相等;equals()不相等的兩個對象,卻並不能證明他們的hashcode()不相等。
換句話說,equals()方法不相等的兩個對象,hashcode()有可能相等(我的理解是由於哈希碼在生成的時候產生沖突造成的)。反過來,hashcode()不等,一定能推出equals()也不等;hashcode()相等,equals()可能相等,也可能不等。

在object類中,hashcode()方法是本地方法,返回的是對象的地址值,而object類中的equals()方法比較的也是兩個對象的地址值,如果equals()相等,說明兩個對象地址值也相等,當然hashcode()也就相等了;在String類中,equals()返回的是兩個對象內容的比較,當兩個對象內容相等時,Hashcode()方法根據String類的重寫代碼的分析,也可知道hashcode()返回結果也會相等。以此類推,可以知道Integer、Double等封裝類中經過重寫的equals()和hashcode()方法也同樣適合於這個原則。當然沒有經過重寫的類,在繼承了object類的equals()和hashcode()方法后,也會遵守這個原則。

Hashset、Hashmap、Hashtable與hashcode()和equals()的密切關系

Hashset是繼承Set接口,Set接口又實現Collection接口,這是層次關系。那么Hashset、Hashmap、Hashtable中的存儲操作是根據什么原理來存取對象的呢?

下面以HashSet為例進行分析,我們都知道:在hashset中不允許出現重復對象,元素的位置也是不確定的。在hashset中又是怎樣判定元素是否重復的呢?在java的集合中,判斷兩個對象是否相等的規則是:
        1.判斷兩個對象的hashCode是否相等

  • 如果不相等,認為兩個對象也不相等,完畢
    如果相等,轉入2
          (這一點只是為了提高存儲效率而要求的,其實理論上沒有也可以,但如果沒有,實際使用時效率會大大降低,所以我們這里將其做為必需的。)

  • 2.判斷兩個對象用equals運算是否相等
           如果不相等,認為兩個對象也不相等
           如果相等,認為兩個對象相等(equals()是判斷兩個對象是否相等的關鍵)

為什么是兩條准則,難道用第一條不行嗎?不行,因為前面已經說了,hashcode()相等時,equals()方法也可能不等,所以必須用第2條准則進行限制,才能保證加入的為非重復元素。

例1:

package com.bijian.study;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
public class HashSetTest {
    public static void main(String args[]) {
        String s1 = new String("aaa");
        String s2 = new String("aaa");
        System.out.println(s1 == s2);
        System.out.println(s1.equals(s2));
        System.out.println(s1.hashCode());
        System.out.println(s2.hashCode());
        Set hashset = new HashSet();
        hashset.add(s1);
        hashset.add(s2);
        Iterator it = hashset.iterator();
        while (it.hasNext()) {
            System.out.println(it.next());
        }
    }
}
運行結果:
false
true
96321
96321
aaa

這是因為String類已經重寫了equals()方法和hashcode()方法,所以hashset認為它們是相等的對象,進行了重復添加。

例2:

package com.bijian.study;
import java.util.HashSet;
import java.util.Iterator;
public class HashSetTest {
    public static void main(String[] args) {
        HashSet hs = new HashSet();
        hs.add(new Student(1, "zhangsan"));
        hs.add(new Student(2, "lisi"));
        hs.add(new Student(3, "wangwu"));
        hs.add(new Student(1, "zhangsan"));
        Iterator it = hs.iterator();
        while (it.hasNext()) {
            System.out.println(it.next());
        }
    }
}
class Student {
    int num;
    String name;
    Student(int num, String name) {
        this.num = num;
        this.name = name;
    }
    public String toString() {
        return num + ":" + name;
    }
}
運行結果:
1:zhangsan  
3:wangwu  
2:lisi  
1:zhangsan

為什么hashset添加了相等的元素呢,這是不是和hashset的原則違背了呢?回答是:沒有。因為在根據hashcode()對兩次建立的new Student(1,“zhangsan”)對象進行比較時,生成的是不同的哈希碼值,所以hashset把他當作不同的對象對待了,當然此時的equals()方法返回的值也不等。

為什么會生成不同的哈希碼值呢?上面我們在比較s1和s2的時候不是生成了同樣的哈希碼嗎?原因就在於我們自己寫的Student類並沒有重新自己的hashcode()和equals()方法,所以在比較時,是繼承的object類中的hashcode()方法,而object類中的hashcode()方法是一個本地方法,比較的是對象的地址(引用地址),使用new方法創建對象,兩次生成的當然是不同的對象了,造成的結果就是兩個對象的hashcode()返回的值不一樣,所以Hashset會把它們當作不同的對象對待。

怎么解決這個問題呢?答案是:在Student類中重新hashcode()和equals()方法。

class Student {
    int num;
    String name;
    Student(int num, String name) {
        this.num = num;
        this.name = name;
    }
    public int hashCode() {
        return num * name.hashCode();
    }
    public boolean equals(Object o) {
        Student s = (Student) o;
        return num == s.num && name.equals(s.name);
    }
    public String toString() {
        return num + ":" + name;
    }
}
運行結果:
1:zhangsan  
3:wangwu  
2:lisi

可以看到重復元素的問題已經消除,根據重寫的方法,即便兩次調用了new Student(1,"zhangsan"),我們在獲得對象的哈希碼時,根據重寫的方法hashcode(),獲得的哈希碼肯定是一樣的,當然根據equals()方法我們也可判斷是相同的,所以在向hashset集合中添加時把它們當作重復元素看待了。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM