Java入門系列之hashCode和equals(十二)

前言

前面兩節內容咱們詳細講解了Hashtable算法和源碼分析,針對散列函數始終逃脫不掉hashCode的計算,本節咱們將詳細分析hashCode和equals,同時您將會看到本節內容是從《Effective Java》學習整理而來(吐槽一句,這本書中文版翻譯的真垃圾),對於《Effective Java》這本書頗有學習價值,可是我不會像其餘童鞋同樣,直接從這本書講解一個系列,我所採用的是學習到對應地方而後參考不一樣java經典書籍進行總結,按部就班式這樣效果更佳,好了,咱們開始吧。java

equals

翻看《Effective Java》關於equals這一節內容,直接拋出重寫equals必須遵照的以下五大約定,當我看到這幾大特性時,頓時驚呆了,這不就是大學線代講解矩陣時的特色麼,學以至用原來是這麼個道理。算法

一、自反性:對於非空的對象x,x.equals(x)必須返回true.ide

二、對稱性:對於非空的對象x和y,若x.equals(y)等於true時,那麼y.equals(x)也必須返回true.函數

三、傳遞性:對於非空的對象x、y和z,若是x.equals(y)和y.equals(z)等於true時,那麼x.equals(z)也必須返回true源碼分析

四、一致性:對於非空的對象x和y,若是利用equals判斷對象的信息沒有被修改時,不管調用多少次,那麼x.equals(y)要麼爲true,要麼爲false性能

五、對於非空的對象x,x.equals(null)必須返回false學習

關於第一點很好理解,非空對象自身引用必須相等,對於第二點書中所給的例子則是將重寫對象比較某個字符串時不區分大小寫,可是字符串對象是區分大小寫,如此這樣將致使對稱不一致問題,對於第三點則是繼承時注意equals的傳遞性,第4點則強調屢次調用經過equals判斷的恆等性,最後一點更好理解如若不判斷則會拋出空指針異常。那麼咱們實際在重寫equals時可將如下幾點做爲模板來使用就能夠啦。this

一、使用「==」判斷兩個對象是否引用相同spa

二、使用instanceof操做符來檢查參數類型是否相同翻譯

三、若類型相同,則將參數轉換爲正確的類型

四、比較對象中每一個值是否都相等,若所有相等則返回true,不然爲false

如上幾點模板來自《Effective Java》對重寫equals的總結,固然咱們能夠從重寫字符串對象中的equals找到如上影子,字符串對象的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;
    }

好了,到這裏咱們講解完了equals,仍是比較簡單,那麼重寫equals時爲什麼必定要重寫hashCode呢?主要緣由在於:這是通用約定,若是是基於散列的集合比較HashMap或者HashSet等,存儲對象地址須要經過散列函數計算hashCode,如若不這樣作將會出現意想不到的問題。那麼意想不到的問題是什麼呢?

hashCode

下面咱們用一個例子來說解爲什麼重寫equals時必定要重寫hashCode。

public class Person {
    int age;
    String name;

    public Person(int age, String name) {
        this.age = age;
        this.name = name;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }

        if (obj instanceof Person) {
            Person p = (Person) obj;
            return (this.age == p.age && this.name == p.name);
        }

        return  false;
    }
}

如上咱們給出一個Person對象,而後帶有年齡和名稱兩個屬性,重寫時判斷年齡和名稱相等便可認爲爲同一人,下面咱們在控制檯進行以下操做,而後咱們看看將會打印出什麼結果呢。

        Person p1 = new Person(12, "Jeffcky");
        Person p2 = new Person(12, "Jeffcky");

        Hashtable hashtable = new Hashtable();
        hashtable.put(p1, "v1");

        System.out.println(hashtable.get(p2));

不難理解,由於Hashtable對象存儲地址是基於hashCode,可是上述咱們沒有重寫hashCode,因此咱們實例化對象p2時,即便重寫了equals兩個對象相等,結果獲取p2的值確定是獲取不到的,由於hashCode不等,接下來咱們重寫hashCode

   @Override
    public int hashCode() {
        return (31 * Integer.valueOf(this.age).hashCode() + name.hashCode());
    }

咱們看到字符串對象重寫了hashCode,由於字符串用的很頻繁,同時咱們極有可能在散列集合中用到。下面咱們來看看字符串對象的hashCode實現方式。

 

 上圖標記出的就是計算字符串的hashCode核心即散列函數,從上看出經過字符串中每個字符的ASCII碼來計算,同時咱們也可再拓展下看源碼數值類型的hashCode就是其自己。上述計算方式最終咱們數學進行概括出計算方法爲:

s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]

好比咱們計算字符串【AC】的hashCode,根據如上計算公式則是

65*31^(2-1) + 67*31^(2-2) = 2082

在《Effective Java》中提到之因此選擇31的緣由是:它是一個奇素數,若是乘數是偶數,而且乘法溢出的話,信息就會丟失,由於2相乘等價於移位運算。使用素數的好處並不很明顯,可是習慣使用素數來計算散列結果。我嚴重懷疑是否是翻譯的人理解錯了意思,對於書中給出選擇素數的緣由沒法讓人折服,這裏我來說解我我的的想法。 

散列函數爲何要使用質數

選擇31的緣由是由於它是質數(素數),而不是由於它是奇數。當咱們插入一個元素到哈希表中時,哈希如何識別須要將元素存儲在哪一個存儲桶中(Bucket)呢?這是一個重要的問題,使得強制性要求哈希可以在恆定時間內告訴咱們將值存儲在哪一個存儲桶中,以便可以快速檢索。咱們能想到的是傻瓜式操做方式即循環遍歷比較,這種順序搜索將直接致使哈希性能惡化,直接取決哈希表所包含值的數量。換句話說,這將具備線性性能成本(O(N)),隨着鍵(N)的數量愈來愈大,性能可想而知。另外一個複雜之處是咱們要處理的值的實際類型。若咱們要處理字符串和其餘複雜類型,檢查或比較自己的數量將致使成本又將變得很高。基於以上敘述,因此咱們至少須要解決兩個問題,其一是便於快速檢索而非順序檢索,其二是解決複雜類型值的比較。解決此問題的簡單方法是但願出現一種將複雜值分解爲易於使用的鍵或哈希的方法,實現此過程的最簡單方法是生成惟一編號,該數字必須是惟一的,由於咱們要區分一個值和另外一個值。質數是惟一數字,它們的獨特之處在於,因爲使用了素數來構成素數,所以素數與任何其餘數字的乘積具備的最大可能的惟一性(不像素數自己那樣惟一),質數的此屬性在哈希函數中使用可減小衝突次數(或碰撞)。例如使用4 * 8,則它比諸如3 * 5的質數乘積更有可能發生衝突,32能夠經過1 * 32或2 * 16或4 * 8或2 ^ 5等計算獲得,但3*5 只能以1 * 15或3 * 5獲得15。

總結 

本文咱們詳細討論了hashCode和equals,以及分析了在散列函數中使用質數的緣由,這裏還存在一節內容留到學習虛擬機時再補上,經過分析虛擬機源碼瞭解hashCode具體實現,下一節咱們將進入學習分析HashMap源碼,感謝您的閱讀,咱們下節見。

相關文章
相關標籤/搜索