通過HashMap認(rèn)識equals與hashcode

什么是hashcode,hashcode的作用是什么

hashcode并不是java中獨(dú)有的。設(shè)想一下,如果讓你設(shè)計(jì)一個(gè)算法,根據(jù)關(guān)鍵碼去得到一個(gè)集合中的某個(gè)值或者這個(gè)關(guān)鍵碼所在的位置。普通的做法就是挨個(gè)比較,高級一點(diǎn)的使用二分檢索或者樹形檢索等算法。但是以上的檢索算法都跟集合的長度N有關(guān),當(dāng)問題規(guī)模N很大時(shí),這些檢索的效率可能十分低下。

理想的情況是,根據(jù)關(guān)鍵碼,我們就可以定位記錄所在的位置,而不用去挨個(gè)進(jìn)行比較。也就是說,在關(guān)鍵碼與記錄存放的位置之間做一種映射。這個(gè)映射的方法就是hash(哈希)函數(shù),或者叫散列函數(shù),也就是java中的hashCode()方法,他所返回的值就是hashcode,根據(jù)hashcode可以找到記錄的位置。

按照散列的存儲方式構(gòu)造的存儲結(jié)構(gòu)叫做散列表。散列表中的一個(gè)位置稱之為一個(gè)槽。

hashCode()方法存在于java.lang.Object類當(dāng)中,任何類都可以繼承修改這個(gè)方法。hashCode()方法返回調(diào)用它的實(shí)例的hashCode值,是個(gè)int值。

注:以下代碼均來自jdk1.7

String中hashCode()方法的實(shí)現(xiàn):

public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
}

什么是equals(Object obj)方法

equals(Object obj)方法同樣來自O(shè)bject類。在Object類中,他是這樣實(shí)現(xiàn)的:

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

也就是說,默認(rèn)的equals(Object obj)方法直接將要比較的兩個(gè)對象的內(nèi)存地址進(jìn)行了比較,一致則返回true。

這個(gè)方法主要用來實(shí)現(xiàn)兩個(gè)對象間的比較,確認(rèn)他們在邏輯上是否相等。我們同樣可以實(shí)現(xiàn)自己的equals(Object obj)方法。

String中equals(Object obj)方法的實(shí)現(xiàn):

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;
}

在java中hashcode方法與equals方法的作用

首先看一下HashMap中的put方法:

public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);//得到hash值
        int i = indexFor(hash, table.length);//找到槽
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

我們從 int hash = hash(key); 這一行看起,這行起才是put方法的核心。

首先 int hash = hash(key); key就是我們之前提到的關(guān)鍵碼,我們看看HashMap中的這個(gè)hash方法做了些什么:

final int hash(Object k) {
        int h = hashSeed;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }

        h ^= k.hashCode();

        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
}

可以看到,這個(gè)方法里調(diào)用了key本身的hashCode方法,得到了key的hashcode,然后對該hashcode進(jìn)行了一些移位操作,最終返回操作后的int值。返回的這個(gè)值就是HashMap要用到的hashcode值,通過他可以找到記錄所在的位置。那么現(xiàn)在有一個(gè)問題:為什么要專門調(diào)用這個(gè)hash(Onject key)方法來對key的hashcode進(jìn)行包裝然后再使用呢?可以直接使用key的hashcode的呀,這樣做看起來不是多此一舉嗎?

其實(shí)這樣做的目的是為下面的函數(shù)做準(zhǔn)備的,我們看接下來要執(zhí)行的代碼:

int i = indexFor(hash, table.length);找到所謂的槽,也就是記錄存在的位置。

/**
  * Returns index for hash code h.
  */
static int indexFor(int h, int length) {
     // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
    return h & (length-1);
}

可以看到,indexFor(int h, int length)如何通過hashcode得到記錄的位置。indexFor方法內(nèi)部是一個(gè)取模運(yùn)算,h是我們通過上面的hash方法得到的,length是散列表的長度。HashMap中的散列表是一個(gè)數(shù)組,通過取模運(yùn)算能保證indexFor方法的返回值(記錄的位置)一定在這個(gè)數(shù)組內(nèi),沒有超過其長度。因?yàn)閔往往是一個(gè)很大的數(shù)字(int可以表示40億這么大的數(shù)字),而散列表的初始長度是可以由我們指定的(默認(rèn)是16),另一方面,就算給他這么大的數(shù)組,內(nèi)存也是放不下的。所以取模運(yùn)算是必須的。經(jīng)過取模運(yùn)算得到的才是真正的槽值。

回到上一個(gè)問題,為什么要專門調(diào)用這個(gè)hash(Onject key)方法來對key的hashcode進(jìn)行包裝然后再使用呢?而不是直接使用key的hashcode的?

想明白這個(gè)問題,參考JDK 源碼中 HashMap 的 hash 方法原理是什么?。我們上面也說了這樣做的目的是為indexFor方法做準(zhǔn)備的,總的來說就是為了讓取模運(yùn)算不會出現(xiàn)一種極端情況:大量的不同的h經(jīng)過取模后返回同樣的槽值。這樣會帶來嚴(yán)重的性能問題,也就是嚴(yán)重的沖突情況導(dǎo)致性能下降。關(guān)于沖突,看下文。

要理解接下來的代碼,我們就需要知道哈希算法的另一個(gè)概念:沖突。

散列函數(shù)可能對于不相等的關(guān)鍵碼計(jì)算出相同的hashcode,該現(xiàn)象稱為沖突。怎么理解呢?

比如我們有這樣一個(gè)串a(chǎn)bcd,我們給出這樣一個(gè)散列函數(shù):將每一個(gè)字符的ascii值加起來除以字符的個(gè)數(shù),得到他們的平均值就是這個(gè)串的hashcode。那么,可以保證沒有其他的串經(jīng)過這樣的算法得到相同的hashcode嗎?也就是說,無限多的元素通過散列函數(shù)映射到有窮集合上,一定會產(chǎn)生沖突。這也是我們理解hashcode的一個(gè)重要的點(diǎn):不同的對象(equals返回false)可以有相同的hashcode

那么,產(chǎn)生沖突怎么辦呢?產(chǎn)生沖突之后,不同的對象在散列表中找到了相同的位置,為了解決這個(gè)問題,我們將這個(gè)槽中的內(nèi)容設(shè)計(jì)成一個(gè)鏈表,當(dāng)產(chǎn)生沖突的時(shí)候,就將新的元素放到鏈表中,他看起來是這樣的:

image.png

其中:A,B,C分別為三條記錄,他們就是產(chǎn)生沖突的三條記錄。1,2,3....為散列表的索引位置。

接下來的代碼 for (Entry<K,V> e = table[i]; e != null; e = e.next)就容易理解了。找到記錄所對應(yīng)的槽之后,遍歷這個(gè)鏈表直到找到與關(guān)鍵碼相同的位置(可能之前已經(jīng)有以這個(gè)關(guān)鍵碼為key的value插入)。如果遍歷完鏈表還沒有找到這樣的值,說明還不存在此關(guān)鍵碼對應(yīng)的記錄,直接插入即可:addEntry(hash, key, value, i);.

那么,怎么判斷兩個(gè)關(guān)鍵碼在邏輯上是否相同呢?

if (e.hash == hash && ((k = e.key) == key || key.equals(k)))

可以看到,首先判斷關(guān)鍵碼的hashcode與鏈表記錄的關(guān)鍵碼hashcode是否相同:·e.hash == hash。為什么要加這樣的判斷?回頭看看indexFor方法,經(jīng)過取模運(yùn)算后,不同的hashcode可以被散列在同一個(gè)槽中。通過這句代碼可以將那些因?yàn)槿∧_\(yùn)算散列到同一個(gè)槽里的不同對象排除。

我們知道不同的對象(equals返回false)可以有相同的hashcode。相同的對象hashcode也必須相等嗎?試想一下,如果兩個(gè)對象相同,但是他們的hashcode不同,那么這兩個(gè)對象很有可能被散列在不同的槽里,造成了同一個(gè)對象重復(fù)存儲的問題。所以,我們又得出一個(gè)重要結(jié)論:相同的對象(equals返回true)hashcode一定相等。

e.hash == hash && ((k = e.key) == key:這段代碼首先判斷hashcode是否相等,然后判斷關(guān)鍵碼是否相等。注意,是判斷關(guān)鍵碼是否相等,直接比較內(nèi)存地址,如果滿足以上條件,那么可以斷定兩個(gè)關(guān)鍵碼相同,是我們要找的記錄。

key.equals(k):如果上述兩個(gè)條件沒有滿足,并不能夠斷定這兩個(gè)關(guān)鍵碼相等,此刻要使用equals方法判斷這兩個(gè)關(guān)鍵碼是否相同。如果相同,說明是我們要找的記錄。

if (e.hash == hash && ((k = e.key) == key || key.equals(k)))這句代碼中其實(shí)包含了一種短路思想,|| 之前的判斷如果生效,那么之后的key.equals(k)就不會再執(zhí)行。很明顯內(nèi)存地址的比較要比equals方法高效的多。這也是Hashmap提高查找效率的一個(gè)重要手段。

至此,我們應(yīng)該對equals和hashcode有了一個(gè)相對清晰的認(rèn)識:hashcode提高了查找指定對象的效率。euqals定義了兩個(gè)對象之間是否在邏輯上相同。hashcode只在HashMap,HashSet等這樣使用了散列思想的地方用到,而equals在判斷兩個(gè)對象之間是否相同時(shí)需要用到,比如排序等。

總結(jié)

通過上面的分析,我們知道了hashcode與equals的幾個(gè)關(guān)鍵:

1.不同的對象(equals返回false)可以有相同的hashcode

2.相同的對象(equals返回true)hashcode一定相等

3.若重新定義了上面兩種方法中的一種,那么另一種方法也需要重新定義(對1、2的遵守)

關(guān)于如何重寫equals方法與hashCode方法

equals與==

"==" 比較的是兩個(gè)對象的內(nèi)存地址,是物理意義上的相等

equals比較的是兩個(gè)對象邏輯意義上的相等,是邏輯意義上的相等

兩個(gè)對象進(jìn)行比較:

== 返回true,則equals一定返回true;

equals返回true,== 不一定返回true。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 實(shí)際上,HashSet 和 HashMap 之間有很多相似之處,對于 HashSet 而言,系統(tǒng)采用 Hash 算...
    曹振華閱讀 2,562評論 1 37
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法,內(nèi)部類的語法,繼承相關(guān)的語法,異常的語法,線程的語...
    子非魚_t_閱讀 34,734評論 18 399
  • 一、基本數(shù)據(jù)類型 注釋 單行注釋:// 區(qū)域注釋:/* */ 文檔注釋:/** */ 數(shù)值 對于byte類型而言...
    龍貓小爺閱讀 4,455評論 0 16
  • 我以為躲進(jìn)洶涌的人潮就能把孤獨(dú)脫結(jié) 我以為借助于黑夜才敢把深埋已久的故事一一揭開 我以為一個(gè)漠然的眼神就能把之前所...
    勞心者閱讀 572評論 3 11
  • 下面直接上代碼 第一種替換Image圖片的方式: using UnityEngine;using System.C...
    上善若水jf閱讀 7,615評論 2 10

友情鏈接更多精彩內(nèi)容