【Java】淺談HashMap

基礎知識

數組:採用一段連續的存儲單元來存儲數據。對於指定下標的查找,時間複雜度爲O(1);通過給定值進行查找,需要遍歷數組,逐一比對給定關鍵字和數組元素,時間複雜度爲O(n),當然,對於有序數組,則可採用二分查找,插值查找,斐波那契查找等方式,可將查找複雜度提高爲O(logn);對於一般的插入刪除操作,涉及到數組元素的移動,其平均複雜度也爲O(n)

線性鏈表:對於鏈表的新增,刪除等操作(在找到指定操作位置後),僅需處理結點間的引用即可,時間複雜度爲O(1),而查找操作需要遍歷鏈表逐一進行比對,複雜度爲O(n)

二叉樹:對一棵相對平衡的有序二叉樹,對其進行插入,查找,刪除等操作,平均複雜度均爲O(logn)。

哈希表:相比上述幾種數據結構,在哈希表中進行添加,刪除,查找等操作,性能十分之高,不考慮哈希衝突的情況下(後面會探討下哈希衝突的情況),僅需一次定位即可完成,時間複雜度爲O(1),接下來我們就來看看哈希表是如何實現達到驚豔的常數階O(1)的。

HashMap實現原理

HashMap的主幹是一個Entry數組。Entry是HashMap的基本組成單元,每一個Entry包含一個key-value鍵值對。(其實所謂Map其實就是保存了兩個對象之間的映射關係的一種集合)

HashMap由數組+鏈表組成的,數組是HashMap的主體,鏈表則是主要爲了解決哈希衝突而存在的,如果定位到的數組位置不含鏈表(當前entry的next指向null),那麼查找,添加等操作很快,僅需一次尋址即可;如果定位到的數組包含鏈表,對於添加操作,其時間複雜度爲O(n),首先遍歷鏈表,存在即覆蓋,否則新增;對於查找操作來講,仍需遍歷鏈表,然後通過key對象的equals方法逐一比對查找。所以,性能考慮,HashMap中的鏈表出現越少,性能纔會越好。

HashMap尋址

1、根據key算出hashCode,這是一個32位的int整數

2、hashCode右移16位,再和自己進行異或操作(相同爲0,不同爲1)

3、最後和數組長度-1進行與操作(都爲1則爲1,否則爲0)

 

當發生哈希衝突並且size大於閾值的時候,需要進行數組擴容,擴容時,需要新建一個長度爲之前數組2倍的新的數組,然後將當前的Entry數組中的元素全部傳輸過去,擴容後的新數組長度爲之前的2倍,所以擴容相對來說是個耗資源的操作

HashMap數組長度

HashMap數組長度一定是2的次冪

1、新數組索引和老數組索引最大程度一致性

假設16的二進制表示爲 10000,那麼length-1就是15,二進制爲01111,同理擴容後的數組長度爲32,二進制表示爲100000,length-1爲31,二進制表示爲011111。從下圖可以我們也能看到這樣會保證低位全爲1,而擴容後只有一位差異,也就是多出了最左位的1,這樣在通過 h&(length-1)的時候,只要h對應的最左邊的那一個差異位爲0,就能保證得到的新的數組索引和老數組索引一致(大大減少了之前已經散列良好的老數組的數據位置重新調換)

2、數組長度保持2的次冪,length-1的低位都爲1,會使得獲得的數組索引index更加均勻 

上面的&運算,高位是不會對結果產生影響的(hash函數採用各種位運算可能也是爲了使得低位更加散列),我們只關注低位bit,如果低位全部爲1,那麼對於h低位部分來說,任何一位的變化都會對結果產生影響,也就是說,要得到index=21這個存儲位置,h的低位只有這一種組合。這也是數組長度設計爲必須爲2的次冪的原因。 

如果不是2的次冪,也就是低位不是全爲1此時,要使得index=21,h的低位部分不再具有唯一性了,因爲h某一位對於的length-1那一位不是1的話,h的那一位可以是0也可以是1,不具有唯一性,哈希衝突的機率會變的更大,同時,index對應的這個bit位無論如何不會等於1了,而對應的那些數組位置也就被白白浪費了。

HashMap查找

key(hashcode)–>hash–>indexFor–>最終索引位置,找到對應位置table[i],再查看是否有鏈表,遍歷鏈表,通過key的equals方法比對查找對應的記錄。

重寫hashCode()和equals()

HashMap的很多函數要基於equal()函數和hashCode()函數。hashCode()用來定位要存放的位置,equal()用來判斷是否相等

Object版本的equal只是簡單地判斷是不是同一個實例。但是有的時候,我們想要的的是邏輯上的相等。比如有一個學生類student,有一個屬性studentID,只要studentID相等,不是同一個實例我們也認爲是同一學生。當我們認爲判定equals的相等應該是邏輯上的相等而不是隻是判斷是不是內存中的同一個東西的時候,就需要重寫equal()。而涉及到HashMap的時候,重寫了equals(),就需要重寫hashCode()
看一下hashCode和equals()的源碼

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

 hashCode()方法是一個本地native方法,返回的是對象引用中存儲的對象的內存地址,而equals方法是利用==來比較的也是對象的內存地址。從上邊我們可以看出,hashCode方法和equals方法是一致的。

1.在 Java 應用程序執行期間,在對同一對象多次調用 hashCode 方法時,必須一致地返回相同的整數,前提是將對象進行 equals 比較時所用的信息沒有被修改。從某一應用程序的一次執行到同一應用程序的另一次執行,該整數無需保持一致。    
2.如果根據 equals(Object) 方法,兩個對象是相等的,那麼對這兩個對象中的每個對象調用 hashCode 方法都必須生成相同的整數結果。    
3.如果根據 equals(java.lang.Object) 方法,兩個對象不相等,那麼對這兩個對象中的任一對象上調用 hashCode 方法不 要求一定生成不同的整數結果。但是,程序員應該意識到,爲不相等的對象生成不同整數結果可以提高哈希表的性能。

JDK1.8中HashMap的性能優化

JDK1.8在JDK1.7的基礎上針對增加了紅黑樹來進行優化。即當鏈表超過8時,鏈表就轉換爲紅黑樹,利用紅黑樹快速增刪改查的特點提高HashMap的性能,其中會用到紅黑樹的插入、刪除、查找等算法。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章