溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

不容錯(cuò)過(guò)的HashMap實(shí)現(xiàn)原理及源碼分析

發(fā)布時(shí)間:2020-08-15 19:49:36 來(lái)源:ITPUB博客 閱讀:148 作者:茶軸的青春 欄目:編程語(yǔ)言

哈希表(hash table)也叫散列表,是一種非常重要的數(shù)據(jù)結(jié)構(gòu),應(yīng)用場(chǎng)景及其豐富,許多緩存技術(shù)(比如memcached)的核心其實(shí)就是在內(nèi)存中維護(hù)一張大的哈希表,而HashMap的實(shí)現(xiàn)原理也常常出現(xiàn)在各類的面試題中,重要性可見一斑。本文會(huì)對(duì)java集合框架中的對(duì)應(yīng)實(shí)現(xiàn)HashMap的實(shí)現(xiàn)原理進(jìn)行講解,然后會(huì)對(duì)JDK7的HashMap源碼進(jìn)行分析。

目錄

  一、 什么是哈希表

  二、 HashMap實(shí)現(xiàn)原理

  三、 為何HashMap的數(shù)組長(zhǎng)度一定是2的次冪?

  四、 重寫equals方法需同時(shí)重寫hashCode方法

  五、 總結(jié)

一、什么是哈希表

  在討論哈希表之前,我們先大概了解下其他數(shù)據(jù)結(jié)構(gòu)在新增,查找等基礎(chǔ)操作執(zhí)行性能

   數(shù)組 :采用一段連續(xù)的存儲(chǔ)單元來(lái)存儲(chǔ)數(shù)據(jù)。對(duì)于指定下標(biāo)的查找,時(shí)間復(fù)雜度為O(1);通過(guò)給定值進(jìn)行查找,需要遍歷數(shù)組,逐一比對(duì)給定關(guān)鍵字和數(shù)組元素,時(shí)間復(fù)雜度為O(n),當(dāng)然,對(duì)于有序數(shù)組,則可采用二分查找,插值查找,斐波那契查找等方式,可將查找復(fù)雜度提高為O(logn);對(duì)于一般的插入刪除操作,涉及到數(shù)組元素的移動(dòng),其平均復(fù)雜度也為O(n)

   線性鏈表 :對(duì)于鏈表的新增,刪除等操作(在找到指定操作位置后),僅需處理結(jié)點(diǎn)間的引用即可,時(shí)間復(fù)雜度為O(1),而查找操作需要遍歷鏈表逐一進(jìn)行比對(duì),復(fù)雜度為O(n)

   二叉樹 :對(duì)一棵相對(duì)平衡的有序二叉樹,對(duì)其進(jìn)行插入,查找,刪除等操作,平均復(fù)雜度均為O(logn)。

   哈希表 :相比上述幾種數(shù)據(jù)結(jié)構(gòu),在哈希表中進(jìn)行添加,刪除,查找等操作,性能十分之高,不考慮哈希沖突的情況下,僅需一次定位即可完成,時(shí)間復(fù)雜度為O(1),接下來(lái)我們就來(lái)看看哈希表是如何實(shí)現(xiàn)達(dá)到驚艷的常數(shù)階O(1)的。

  我們知道,數(shù)據(jù)結(jié)構(gòu)的物理存儲(chǔ)結(jié)構(gòu)只有兩種: 順序存儲(chǔ)結(jié)構(gòu) 鏈?zhǔn)酱鎯?chǔ)結(jié)構(gòu) (像棧,隊(duì)列,樹,圖等是從邏輯結(jié)構(gòu)去抽象的,映射到內(nèi)存中,也這兩種物理組織形式),而在上面我們提到過(guò),在數(shù)組中根據(jù)下標(biāo)查找某個(gè)元素,一次定位就可以達(dá)到,哈希表利用了這種特性, 哈希表的主干就是數(shù)組 。

  比如我們要新增或查找某個(gè)元素,我們通過(guò)把當(dāng)前元素的關(guān)鍵字 通過(guò)某個(gè)函數(shù)映射到數(shù)組中的某個(gè)位置,通過(guò)數(shù)組下標(biāo)一次定位就可完成操作。

         存儲(chǔ)位置 = f(關(guān)鍵字)

   其中,這個(gè)函數(shù)f一般稱為 哈希函數(shù) ,這個(gè)函數(shù)的設(shè)計(jì)好壞會(huì)直接影響到哈希表的優(yōu)劣。舉個(gè)例子,比如我們要在哈希表中執(zhí)行插入操作:

   不容錯(cuò)過(guò)的HashMap實(shí)現(xiàn)原理及源碼分析

  查找操作同理,先通過(guò)哈希函數(shù)計(jì)算出實(shí)際存儲(chǔ)地址,然后從數(shù)組中對(duì)應(yīng)地址取出即可。

   哈希沖突

  然而萬(wàn)事無(wú)完美,如果兩個(gè)不同的元素,通過(guò)哈希函數(shù)得出的實(shí)際存儲(chǔ)地址相同怎么辦?也就是說(shuō),當(dāng)我們對(duì)某個(gè)元素進(jìn)行哈希運(yùn)算,得到一個(gè)存儲(chǔ)地址,然后要進(jìn)行插入的時(shí)候,發(fā)現(xiàn)已經(jīng)被其他元素占用了,其實(shí)這就是所謂的 哈希沖突 ,也叫哈希碰撞。前面我們提到過(guò),哈希函數(shù)的設(shè)計(jì)至關(guān)重要,好的哈希函數(shù)會(huì)盡可能地保證  計(jì)算簡(jiǎn)單 散列地址分布均勻, 但是,我們需要清楚的是,數(shù)組是一塊連續(xù)的固定長(zhǎng)度的內(nèi)存空間,再好的哈希函數(shù)也不能保證得到的存儲(chǔ)地址絕對(duì)不發(fā)生沖突。那么哈希沖突如何解決呢?哈希沖突的解決方案有多種:開放定址法(發(fā)生沖突,繼續(xù)尋找下一塊未被占用的存儲(chǔ)地址),再散列函數(shù)法,鏈地址法,而HashMap即是采用了鏈地址法,也就是 數(shù)組+鏈表 的方式,

二、HashMap實(shí)現(xiàn)原理

 HashMap的主干是一個(gè)Entry數(shù)組。Entry是HashMap的基本組成單元,每一個(gè)Entry包含一個(gè)key-value鍵值對(duì)。

//HashMap的主干數(shù)組,可以看到就是一個(gè)Entry數(shù)組,初始值為空數(shù)組{},主干數(shù)組的長(zhǎng)度一定是2的次冪,至于為什么這么做,后面會(huì)有詳細(xì)分析。
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

 Entry是HashMap中的一個(gè)靜態(tài)內(nèi)部類。代碼如下

static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;//存儲(chǔ)指向下一個(gè)Entry的引用,單鏈表結(jié)構(gòu)
int hash;//對(duì)key的hashcode值進(jìn)行hash運(yùn)算后得到的值,存儲(chǔ)在Entry,避免重復(fù)計(jì)算
/**
         * Creates new entry.
         */
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}

 所以,HashMap的整體結(jié)構(gòu)如下

不容錯(cuò)過(guò)的HashMap實(shí)現(xiàn)原理及源碼分析   

   簡(jiǎn)單來(lái)說(shuō),HashMap由數(shù)組+鏈表組成的,數(shù)組是HashMap的主體,鏈表則是主要為了解決哈希沖突而存在的,如果定位到的數(shù)組位置不含鏈表(當(dāng)前entry的next指向null),那么對(duì)于查找,添加等操作很快,僅需一次尋址即可;如果定位到的數(shù)組包含鏈表,對(duì)于添加操作,其時(shí)間復(fù)雜度為O(n),首先遍歷鏈表,存在即覆蓋,否則新增;對(duì)于查找操作來(lái)講,仍需遍歷鏈表,然后通過(guò)key對(duì)象的equals方法逐一比對(duì)查找。所以,性能考慮,HashMap中的鏈表出現(xiàn)越少,性能才會(huì)越好。

其他幾個(gè)重要字段

//實(shí)際存儲(chǔ)的key-value鍵值對(duì)的個(gè)數(shù)
transient int size;
//閾值,當(dāng)table == {}時(shí),該值為初始容量(初始容量默認(rèn)為16);當(dāng)table被填充了,也就是為table分配內(nèi)存空間后,threshold一般為 capacity*loadFactory。HashMap在進(jìn)行擴(kuò)容時(shí)需要參考threshold,后面會(huì)詳細(xì)談到
int threshold;
//負(fù)載因子,代表了table的填充度有多少,默認(rèn)是0.75
final float loadFactor;
//用于快速失敗,由于HashMap非線程安全,在對(duì)HashMap進(jìn)行迭代時(shí),如果期間其他線程的參與導(dǎo)致HashMap的結(jié)構(gòu)發(fā)生變化了(比如put,remove等操作),需要拋出異常ConcurrentModificationException
transient int modCount;

HashMap有4個(gè)構(gòu)造器,其他構(gòu)造器如果用戶沒(méi)有傳入initialCapacity 和loadFactor這兩個(gè)參數(shù),會(huì)使用默認(rèn)值

initialCapacity默認(rèn)為16,loadFactory默認(rèn)為0.75

我們看下其中一個(gè)

public HashMap(int initialCapacity, float loadFactor) {
     //此處對(duì)傳入的初始容量進(jìn)行校驗(yàn),最大不能超過(guò)MAXIMUM_CAPACITY = 1<<30(230)
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
threshold = initialCapacity;
     
init();//init方法在HashMap中沒(méi)有實(shí)際實(shí)現(xiàn),不過(guò)在其子類如 linkedHashMap中就會(huì)有對(duì)應(yīng)實(shí)現(xiàn)
}

 

  從上面這段代碼我們可以看出, 在常規(guī)構(gòu)造器中,沒(méi)有為數(shù)組table分配內(nèi)存空間(有一個(gè)入?yún)橹付∕ap的構(gòu)造器例外),而是在執(zhí)行put操作的時(shí)候才真正構(gòu)建table數(shù)組

  OK,接下來(lái)我們來(lái)看看put操作的實(shí)現(xiàn)吧

public V put(K key, V value) {
//如果table數(shù)組為空數(shù)組{},進(jìn)行數(shù)組填充(為table分配實(shí)際內(nèi)存空間),入?yún)閠hreshold,此時(shí)threshold為initialCapacity 默認(rèn)是1<<4(24=16)
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
//如果key為null,存儲(chǔ)位置為table[0]或table[0]的沖突鏈上
if (key == null)
return putForNullKey(value);
int hash = hash(key);//對(duì)key的hashcode進(jìn)一步計(jì)算,確保散列均勻
int i = indexFor(hash, table.length);//獲取在table中的實(shí)際位置
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
//如果該對(duì)應(yīng)數(shù)據(jù)已存在,執(zhí)行覆蓋操作。用新value替換舊value,并返回舊value
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++;//保證并發(fā)訪問(wèn)時(shí),若HashMap內(nèi)部結(jié)構(gòu)發(fā)生變化,快速響應(yīng)失敗
addEntry(hash, key, value, i);//新增一個(gè)entry
return null;
}

 

 先來(lái)看看inflateTable這個(gè)方法

private void inflateTable(int toSize) {
int capacity = roundUpToPowerOf2(toSize);//capacity一定是2的次冪
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);//此處為threshold賦值,取capacity*loadFactor和MAXIMUM_CAPACITY+1的最小值,capaticy一定不會(huì)超過(guò)MAXIMUM_CAPACITY,除非loadFactor大于1
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}

 

  inflateTable這個(gè)方法用于為主干數(shù)組table在內(nèi)存中分配存儲(chǔ)空間,通過(guò)roundUpToPowerOf2(toSize)可以確保capacity為大于或等于toSize的最接近toSize的二次冪,比如toSize=13,則capacity=16;to_size=16,capacity=16;to_size=17,capacity=32.

private static int roundUpToPowerOf2(int number) {
// assert number >= 0 : "number must be non-negative";
return number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}

roundUpToPowerOf2中的這段處理使得數(shù)組長(zhǎng)度一定為2的次冪,Integer.highestOneBit是用來(lái)獲取最左邊的bit(其他bit位為0)所代表的數(shù)值.

hash函數(shù)

//這是一個(gè)神奇的函數(shù),用了很多的異或,移位等運(yùn)算,對(duì)key的hashcode進(jìn)一步進(jìn)行計(jì)算以及二進(jìn)制位的調(diào)整等來(lái)保證最終獲取的存儲(chǔ)位置盡量分布均勻
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash42((String) k);
}
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}

 

以上hash函數(shù)計(jì)算出的值,通過(guò)indexFor進(jìn)一步處理來(lái)獲取實(shí)際的存儲(chǔ)位置

/**
     * 返回?cái)?shù)組下標(biāo)
     */
static int indexFor(int h, int length) {
return h & (length-1);
}

h&(length-1)保證獲取的index一定在數(shù)組范圍內(nèi),舉個(gè)例子,默認(rèn)容量16,length-1=15,h=18,轉(zhuǎn)換成二進(jìn)制計(jì)算為

        1  0  0  1  0
    &   0  1  1  1  1
    __________________
        0  0  0  1  0    = 2

  最終計(jì)算出的index=2。有些版本的對(duì)于此處的計(jì)算會(huì)使用 取模運(yùn)算,也能保證index一定在數(shù)組范圍內(nèi),不過(guò)位運(yùn)算對(duì)計(jì)算機(jī)來(lái)說(shuō),性能更高一些(HashMap中有大量位運(yùn)算)

所以最終存儲(chǔ)位置的確定流程是這樣的:

不容錯(cuò)過(guò)的HashMap實(shí)現(xiàn)原理及源碼分析

再來(lái)看看addEntry的實(shí)現(xiàn):

void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);//當(dāng)size超過(guò)臨界閾值threshold,并且即將發(fā)生哈希沖突時(shí)進(jìn)行擴(kuò)容
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}

 

  通過(guò)以上代碼能夠得知,當(dāng)發(fā)生哈希沖突并且size大于閾值的時(shí)候,需要進(jìn)行數(shù)組擴(kuò)容,擴(kuò)容時(shí),需要新建一個(gè)長(zhǎng)度為之前數(shù)組2倍的新的數(shù)組,然后將當(dāng)前的Entry數(shù)組中的元素全部傳輸過(guò)去,擴(kuò)容后的新數(shù)組長(zhǎng)度為之前的2倍,所以擴(kuò)容相對(duì)來(lái)說(shuō)是個(gè)耗資源的操作。

三、為何HashMap的數(shù)組長(zhǎng)度一定是2的次冪?

我們來(lái)繼續(xù)看上面提到的resize方法

void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

 

如果數(shù)組進(jìn)行擴(kuò)容,數(shù)組長(zhǎng)度發(fā)生變化,而存儲(chǔ)位置 index = h&(length-1),index也可能會(huì)發(fā)生變化,需要重新計(jì)算index,我們先來(lái)看看transfer這個(gè)方法

void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
     //for循環(huán)中的代碼,逐個(gè)遍歷鏈表,重新計(jì)算索引位置,將老數(shù)組數(shù)據(jù)復(fù)制到新數(shù)組中去(數(shù)組不存儲(chǔ)實(shí)際數(shù)據(jù),所以僅僅是拷貝引用而已)
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
          //將當(dāng)前entry的next鏈指向新的索引位置,newTable[i]有可能為空,有可能也是個(gè)entry鏈,如果是entry鏈,直接在鏈表頭部插入。
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}

 

  這個(gè)方法將老數(shù)組中的數(shù)據(jù)逐個(gè)鏈表地遍歷,扔到新的擴(kuò)容后的數(shù)組中,我們的數(shù)組索引位置的計(jì)算是通過(guò) 對(duì)key值的hashcode進(jìn)行hash擾亂運(yùn)算后,再通過(guò)和 length-1進(jìn)行位運(yùn)算得到最終數(shù)組索引位置。

  hashMap的數(shù)組長(zhǎng)度一定保持2的次冪,比如16的二進(jìn)制表示為 10000,那么length-1就是15,二進(jìn)制為01111,同理擴(kuò)容后的數(shù)組長(zhǎng)度為32,二進(jìn)制表示為100000,length-1為31,二進(jìn)制表示為011111。從下圖可以我們也能看到這樣會(huì)保證低位全為1,而擴(kuò)容后只有一位差異,也就是多出了最左位的1,這樣在通過(guò) h&(length-1)的時(shí)候,只要h對(duì)應(yīng)的最左邊的那一個(gè)差異位為0,就能保證得到的新的數(shù)組索引和老數(shù)組索引一致(大大減少了之前已經(jīng)散列良好的老數(shù)組的數(shù)據(jù)位置重新調(diào)換),個(gè)人理解。

   不容錯(cuò)過(guò)的HashMap實(shí)現(xiàn)原理及源碼分析

 還有,數(shù)組長(zhǎng)度保持2的次冪,length-1的低位都為1,會(huì)使得獲得的數(shù)組索引index更加均勻,比如:

不容錯(cuò)過(guò)的HashMap實(shí)現(xiàn)原理及源碼分析

  我們看到,上面的&運(yùn)算,高位是不會(huì)對(duì)結(jié)果產(chǎn)生影響的(hash函數(shù)采用各種位運(yùn)算可能也是為了使得低位更加散列),我們只關(guān)注低位bit,如果低位全部為1,那么對(duì)于h低位部分來(lái)說(shuō),任何一位的變化都會(huì)對(duì)結(jié)果產(chǎn)生影響,也就是說(shuō),要得到index=21這個(gè)存儲(chǔ)位置,h的低位只有這一種組合。這也是數(shù)組長(zhǎng)度設(shè)計(jì)為必須為2的次冪的原因。

不容錯(cuò)過(guò)的HashMap實(shí)現(xiàn)原理及源碼分析

  如果不是2的次冪,也就是低位不是全為1此時(shí),要使得index=21,h的低位部分不再具有唯一性了,哈希沖突的幾率會(huì)變的更大,同時(shí),index對(duì)應(yīng)的這個(gè)bit位無(wú)論如何不會(huì)等于1了,而對(duì)應(yīng)的那些數(shù)組位置也就被白白浪費(fèi)了。

get方法

public V get(Object key) {
     //如果key為null,則直接去table[0]處去檢索即可。
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}

get方法通過(guò)key值返回對(duì)應(yīng)value,如果key為null,直接去table[0]處檢索。我們?cè)倏匆幌耮etEntry這個(gè)方法

final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
//通過(guò)key的hashcode值計(jì)算hash值
int hash = (key == null) ? 0 : hash(key);
//indexFor (hash&length-1) 獲取最終數(shù)組索引,然后遍歷鏈表,通過(guò)equals方法比對(duì)找出對(duì)應(yīng)記錄
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}

  可以看出,get方法的實(shí)現(xiàn)相對(duì)簡(jiǎn)單,key(hashcode)-->hash-->indexFor-->最終索引位置,找到對(duì)應(yīng)位置table[i],再查看是否有鏈表,遍歷鏈表,通過(guò)key的equals方法比對(duì)查找對(duì)應(yīng)的記錄。要注意的是,有人覺(jué)得上面在定位到數(shù)組位置之后然后遍歷鏈表的時(shí)候,e.hash == hash這個(gè)判斷沒(méi)必要,僅通過(guò)equals判斷就可以。其實(shí)不然,試想一下,如果傳入的key對(duì)象重寫了equals方法卻沒(méi)有重寫hashCode,而恰巧此對(duì)象定位到這個(gè)數(shù)組位置,如果僅僅用equals判斷可能是相等的,但其hashCode和當(dāng)前對(duì)象不一致,這種情況,根據(jù)Object的hashCode的約定,不能返回當(dāng)前對(duì)象,而應(yīng)該返回null,后面的例子會(huì)做出進(jìn)一步解釋。

四、重寫equals方法需同時(shí)重寫hashCode方法

  關(guān)于HashMap的源碼分析就介紹到這兒了,最后我們?cè)倭牧睦仙U劦囊粋€(gè)問(wèn)題,各種資料上都會(huì)提到,“重寫equals時(shí)也要同時(shí)覆蓋hashcode”,我們舉個(gè)小例子來(lái)看看,如果重寫了equals而不重寫hashcode會(huì)發(fā)生什么樣的問(wèn)題

/**
 * Created by chengxiao on 2016/11/15.
 */
public class MyTest {
private static class Person{
int idCard;
String name;
public Person(int idCard, String name) {
this.idCard = idCard;
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()){
return false;
}
Person person = (Person) o;
//兩個(gè)對(duì)象是否等值,通過(guò)idCard來(lái)確定
return this.idCard == person.idCard;
}
}
public static void main(String []args){
HashMap<Person,String> map = new HashMap<Person, String>();
Person person = new Person(1234,"喬峰");
//put到hashmap中去
map.put(person,"天龍八部");
//get取出,從邏輯上講應(yīng)該能輸出“天龍八部”
System.out.println("結(jié)果:"+map.get(new Person(1234,"蕭峰")));
}
}

實(shí)際輸出結(jié)果:

結(jié)果:null

  如果我們已經(jīng)對(duì)HashMap的原理有了一定了解,這個(gè)結(jié)果就不難理解了。盡管我們?cè)谶M(jìn)行g(shù)et和put操作的時(shí)候,使用的key從邏輯上講是等值的(通過(guò)equals比較是相等的),但由于沒(méi)有重寫hashCode方法,所以put操作時(shí),key(hashcode1)-->hash-->indexFor-->最終索引位置 ,而通過(guò)key取出value的時(shí)候 key(hashcode1)-->hash-->indexFor-->最終索引位置,由于hashcode1不等于hashcode2,導(dǎo)致沒(méi)有定位到一個(gè)數(shù)組位置而返回邏輯上錯(cuò)誤的值null(也有可能碰巧定位到一個(gè)數(shù)組位置,但是也會(huì)判斷其entry的hash值是否相等,上面get方法中有提到。)

  所以,在重寫equals的方法的時(shí)候,必須注意重寫hashCode方法,同時(shí)還要保證通過(guò)equals判斷相等的兩個(gè)對(duì)象,調(diào)用hashCode方法要返回同樣的整數(shù)值。而如果equals判斷不相等的兩個(gè)對(duì)象,其hashCode可以相同(只不過(guò)會(huì)發(fā)生哈希沖突,應(yīng)盡量避免)。

五、總結(jié)

  本文描述了HashMap的實(shí)現(xiàn)原理,并結(jié)合源碼做了進(jìn)一步的分析,也涉及到一些源碼細(xì)節(jié)設(shè)計(jì)緣由,最后簡(jiǎn)單介紹了為什么重寫equals的時(shí)候需要重寫hashCode方法。希望本篇文章能幫助到大家,同時(shí)也歡迎討論指正,謝謝支持!


向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI