Java面試題之HashMap阿里面試必問知識點,你會嗎?


面試官Q1:你用過HashMap,你能跟我說說它的數據結構嗎?

HashMap作為一種容器類型,無論你是否了解過其內部的實現原理,它的大名已經頻頻出現在各種互聯網Java面試題中了。從基本的使用角度來說,它很簡單,但從其內部的實現來看,它又並非想象中那么容易。如果你一定要問了解其內部實現與否對於寫程序究竟有多大影響,我不能給出一個確切的答案。但是作為一名合格程序員,對於這種遍地都在談論的技術不應該不為所動。下面我們將自己實現一個簡易版HashMap,然后通過閱讀HashMap的源碼逐步來認識HashMap的底層數據結構。

 

簡易HashMap V1.0版本

V1.0版本我們需要實現Map的幾個重要的功能:

  • 可以存放鍵值對

  • 可以根據鍵查找到值

  • 鍵不能重復

 1public class CustomHashMap {
2    CustomEntry[] arr = new CustomEntry[990];
3    int size;
4
5    public void put(Object key, Object value{
6        CustomEntry e = new CustomEntry(key, value);
7        for (int i = 0; i < size; i++) {
8            if (arr[i].key.equals(key)) {
9                //如果有key值相等,直接覆蓋value
10                arr[i].value = value;
11                return;
12            }
13        }
14        arr[size++] = e;
15    }
16
17    public Object get(Object key{
18        for (int i = 0; i < size; i++) {
19            if (arr[i].key.equals(key)) {
20                return arr[i].value;
21            }
22        }
23        return null;
24    }
25
26    public boolean containsKey(Object key{
27        for (int i = 0; i < size; i++) {
28            if (arr[i].key.equals(key)) {
29                return true;
30            }
31        }
32        return false;
33    }
34
35    public static void main(String[] args{
36        CustomHashMap map = new CustomHashMap();
37        map.put("k1""v1");
38        map.put("k2""v2");
39        map.put("k2""v4");
40        System.out.println(map.get("k2"));
41    }
42
43}
44
45class CustomEntry {
46    Object key;
47    Object value;
48
49    public CustomEntry(Object key, Object value{
50        super();
51        this.key = key;
52        this.value = value;
53    }
54
55    public Object getKey() {
56        return key;
57    }
58
59    public void setKey(Object key{
60        this.key = key;
61    }
62
63    public Object getValue() {
64        return value;
65    }
66
67    public void setValue(Object value{
68        this.value = value;
69    }
70
71}

上面就是我們自定義的簡單Map實現,可以完成V1.0提出的幾個功能點,但是大家有木有發現,這個Map是基於數組實現的,不管是put還是get方法,每次都要循環去做數據的對比,可想而知效率會很低,現在數組長度只有990,那如果數組的長度很長了,豈不是要循環很多次。既然問題出現了,我們有沒有更好的辦法做改進,使得效率提升,答案是肯定,下面就是V2.0版本升級。

 

簡易HashMap V2.0版本

V2.0版本需要處理問題如下:

  • 減少遍歷次數,提升存取數據效率

在做改進之前,我們先思考一下,有沒有什么方式可以在我們放數據的時候,通過一次定位,就能將這個數放到某個位置,而再我們獲取數據的時候,直接通過一次定位就能找到我們想要的數據,那樣我們就減少了很多迭代遍歷次數。

接下來,我們需要介紹一下哈希表的相關知識

在討論哈希表之前,我們先大概了解下其他數據結構在新增,查找等基礎操作執行性能

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

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

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

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

我們知道,數據結構的物理存儲結構只有兩種:順序存儲結構和鏈式存儲結構(像棧,隊列,樹,圖等是從邏輯結構去抽象的,映射到內存中,也這兩種物理組織形式),而在上面我們提到過,在數組中根據下標查找某個元素,一次定位就可以達到,哈希表利用了這種特性,哈希表的主干就是數組。

比如我們要新增或查找某個元素,我們通過把當前元素的關鍵字 通過某個函數映射到數組中的某個位置,通過數組下標一次定位就可完成操作。

存儲位置 = f(關鍵字)

 

其中,這個函數f一般稱為哈希函數,這個函數的設計好壞會直接影響到哈希表的優劣。舉個例子,比如我們要在哈希表中執行插入操作:

查找操作同理,先通過哈希函數計算出實際存儲地址,然后從數組中對應地址取出即可。既然思路有了,那我們繼續改進唄!

 1public class CustomHashMap {
2    CustomEntry[] arr = new CustomEntry[999];
3
4    public void put(Object key, Object value{
5        CustomEntry entry = new CustomEntry(key, value);
6        //使用Hash碼對999取余數,那么余數的范圍肯定在0到998之間
7        //你可能也發現了,不管怎么取余數,余數也會有沖突的時候(暫時先不考慮,后面慢慢道來)
8        //至少現在我們存數據的效率明顯提升了,key.hashCode() % 999 相同的key算出來的結果肯定是一樣的
9        int a = key.hashCode() % 999;
10        arr[a] = entry;
11    }
12
13    public Object get(Object key{
14        //取數的時候也通過一次定位就找到了數據,效率明顯得到提升
15        return arr[key.hashCode() % 999].value;
16    }
17
18    public static void main(String[] args{
19        CustomHashMap map = new CustomHashMap();
20        map.put("k1""v1");
21        map.put("k2""v2");
22        System.out.println(map.get("k2"));
23    }
24
25}
26
27class CustomEntry {
28    Object key;
29    Object value;
30
31    public CustomEntry(Object key, Object value{
32        super();
33        this.key = key;
34        this.value = value;
35    }
36
37    public Object getKey() {
38        return key;
39    }
40
41    public void setKey(Object key{
42        this.key = key;
43    }
44
45    public Object getValue() {
46        return value;
47    }
48
49    public void setValue(Object value{
50        this.value = value;
51    }
52}

通過上面的代碼,我們知道余數也有沖突的時候,不一樣的key計算出相同的地址,那么這個時候我們又要怎么處理呢?

 

哈希沖突

如果兩個不同的元素,通過哈希函數得出的實際存儲地址相同怎么辦?也就是說,當我們對某個元素進行哈希運算,得到一個存儲地址,然后要進行插入的時候,發現已經被其他元素占用了,其實這就是所謂的哈希沖突,也叫哈希碰撞。前面我們提到過,哈希函數的設計至關重要,好的哈希函數會盡可能地保證 計算簡單和散列地址分布均勻,但是,我們需要清楚的是,數組是一塊連續的固定長度的內存空間,再好的哈希函數也不能保證得到的存儲地址絕對不發生沖突。那么哈希沖突如何解決呢?哈希沖突的解決方案有多種:開放定址法(發生沖突,繼續尋找下一塊未被占用的存儲地址),再散列函數法,鏈地址法,而HashMap即是采用了鏈地址法,也就是數組+鏈表的方式

 

通過上面的說明知道,HashMap的底層是基於數組+鏈表的方式,此時,我們需要再對V2.0的Map再次升級

 

簡易HashMap V3.0版本

V3.0版本需要處理問題如下:

  • 存取數據的結構改進

代碼如下:

 1public class CustomHashMap {
2    LinkedList[] arr = new LinkedList[999];
3
4    public void put(Object key, Object value{
5        CustomEntry entry = new CustomEntry(key, value);
6        int a = key.hashCode() % arr.length;
7        if (arr[a] == null) {
8            LinkedList list = new LinkedList();
9            list.add(entry);
10            arr[a] = list;
11        } else {
12            LinkedList list = arr[a];
13            for (int i = 0; i < list.size(); i++) {
14                CustomEntry e = (CustomEntry) list.get(i);
15                if (entry.key.equals(key)) {
16                    e.value = value;// 鍵值重復需要覆蓋
17                    return;
18                }
19            }
20            arr[a].add(entry);
21        }
22    }
23
24    public Object get(Object key{
25        int a = key.hashCode() % arr.length;
26        if (arr[a] != null) {
27            LinkedList list = arr[a];
28            for (int i = 0; i < list.size(); i++) {
29                CustomEntry entry = (CustomEntry) list.get(i);
30                if (entry.key.equals(key)) {
31                    return entry.value;
32                }
33            }
34        }
35        return null;
36    }
37
38    public static void main(String[] args{
39        CustomHashMap map = new CustomHashMap();
40        map.put("k1""v1");
41        map.put("k2""v2");
42        map.put("k2""v3");
43        System.out.println(map.get("k2"));
44    }
45
46}
47
48class CustomEntry {
49    Object key;
50    Object value;
51
52    public CustomEntry(Object key, Object value{
53        super();
54        this.key = key;
55        this.value = value;
56    }
57
58    public Object getKey() {
59        return key;
60    }
61
62    public void setKey(Object key{
63        this.key = key;
64    }
65
66    public Object getValue() {
67        return value;
68    }
69
70    public void setValue(Object value{
71        this.value = value;
72    }
73
74}

最終的數據結構如下:

 

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

 

HashMap源碼

從上面的推導過程,我們逐漸清晰的認識了HashMap的實現原理,下面我們通過閱讀部分源碼,來看看HashMap(基於JDK1.7版本)

1transient Entry[] table;  
2
3static class Entry<K,Vimplements Map.Entry<K,V{  
4    final K key;  
5    V value;  
6    Entry<K,V> next;  
7    final int hash;  
8    ...
9}  

 

可以看出,HashMap中維護了一個Entry為元素的table,transient修飾表示不參與序列化。每個Entry元素存儲了指向下一個元素的引用,構成了鏈表。

put方法實現

 1public V put(K key, V value{  
2    // HashMap允許存放null鍵和null值。  
3    // 當key為null時,調用putForNullKey方法,將value放置在數組第一個位置。  
4    if (key == null)  
5        return putForNullKey(value);  
6    // 根據key的keyCode重新計算hash值。  
7    int hash = hash(key.hashCode());  
8    // 搜索指定hash值在對應table中的索引。  
9    int i = indexFor(hash, table.length);  
10    // 如果 i 索引處的 Entry 不為 null,通過循環不斷遍歷 e 元素的下一個元素。  
11    for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
12        Object k;  
13        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
14            V oldValue = e.value;  
15            e.value = value;  
16            e.recordAccess(this);  
17            return oldValue;  
18        }  
19    }  
20    // 如果i索引處的Entry為null,表明此處還沒有Entry。  
21    modCount++;  
22    // 將key、value添加到i索引處。  
23    addEntry(hash, key, value, i);  
24    return null;  
25}  

從源碼可以看出,大致過程是,當我們向HashMap中put一個元素時,首先判斷key是否為null,不為null則根據key的hashCode,重新獲得hash值,根據hash值通過indexFor方法獲取元素對應哈希桶的索引,遍歷哈希桶中的元素,如果存在元素與key的hash值相同以及key相同,則更新原entry的value值;如果不存在相同的key,則將新元素從頭部插入。如果數組該位置上沒有元素,就直接將該元素放到此數組中的該位置上。

看一下重hash的方法:

1static int hash(int h) {  
2    h ^= (h >>> 20) ^ (h >>> 12);  
3    return h ^ (h >>> 7) ^ (h >>> 4);  
4}  

此算法加入了高位計算,防止低位不變,高位變化時,造成的hash沖突。在HashMap中,我們希望元素盡可能的離散均勻的分布到每一個hash桶中,因此,這邊給出了一個indexFor方法:

1static int indexFor(int h, int length) {  
2    return h & (length-1);  
3}  

這段代碼使用 & 運算代替取模(上面我們自己實現的方式就是取模),效率更高。 

再來看一眼addEntry方法:

 1void addEntry(int hash, K key, V valueint bucketIndex{  
2    // 獲取指定 bucketIndex 索引處的 Entry   
3    Entry<K,V> e = table[bucketIndex];  
4    // 將新創建的 Entry 放入 bucketIndex 索引處,並讓新的 Entry 指向原來的 Entry  
5    table[bucketIndex] = new Entry<K,V>(hash, key, value, e);  
6    // 如果 Map 中的 key-value 對的數量超過了極限  
7    if (size++ >= threshold)  
8    // 把 table 對象的長度擴充到原來的2倍。  
9        resize(2 * table.length);  
10}   

很明顯,這邊代碼做的事情就是從頭插入新元素;如果size超過了閾值threshold,就調用resize方法擴容兩倍,至於,為什么要擴容成原來的2倍,請參考,此節不是我們要說的重點。

 

get方法實現

 1public V get(Object key{  
2    if (key == null)  
3        return getForNullKey();  
4    int hash = hash(key.hashCode());  
5    for (Entry<K,V> e = table[indexFor(hash, table.length)];  
6        e != null;  
7        e = e.next) {  
8        Object k;  
9        if (e.hash == hash && ((k = e.key) == key || key.equals(k)))  
10            return e.value;  
11    }  
12    return null;  
13}   

這段代碼很容易理解,首先根據key的hashCode計算hash值,根據hash值確定桶的位置,然后遍歷。

 

現在,大家都應該對HashMap的底層結構有了更深刻的認識吧,下面筆者對於面試時可能出現的關於HashMap相關的面試題,做了一下梳理,大致如下:

  • 你了解HashMap的底層數據結構嗎?(本文已做梳理)

  • 為何HashMap的數組長度一定是2的次冪?

  • HashMap何時擴容以及它的擴容機制?

  • HashMap的鍵一般使用的String類型,還可以用別的對象嗎?

  • HashMap是線程安全的嗎,如何實現線程安全?

 


免責聲明!

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



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