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

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

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

 

簡易HashMap V1.0版本算法

V1.0版本咱們須要實現Map的幾個重要的功能:數組

  • 能夠存放鍵值對安全

  • 能夠根據鍵查找到值數據結構

  • 鍵不能重複app

 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版本須要處理問題以下:this

  • 減小遍歷次數,提高存取數據效率

在作改進以前,咱們先思考一下,有沒有什麼方式能夠在咱們放數據的時候,經過一次定位,就能將這個數放到某個位置,而再咱們獲取數據的時候,直接經過一次定位就能找到咱們想要的數據,那樣咱們就減小了不少迭代遍歷次數。

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

在討論哈希表以前,咱們先大概瞭解下其餘數據結構在新增,查找等基礎操做執行性能

數組:採用一段連續的存儲單元來存儲數據。對於指定下標的查找,時間複雜度爲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是線程安全的嗎,如何實現線程安全?

相關文章
相關標籤/搜索