1. 程式人生 > >面試----集合HashMap----怎麼答(每一行都畫重點)

面試----集合HashMap----怎麼答(每一行都畫重點)

先說說HashMap

1:首先HashMap 是一個散列表,它儲存的內容是鍵值對(key-value)對映,HashMap中的對映不是有序的。HashMap 繼承於AbstractMap,實現了Map、Cloneable、java.io.Serializable介面。不是執行緒安全的。HashMap是最常用的Map,它根據HashCode值儲存資料,根絕鍵可以直接獲取值。允許有空(null)的鍵值(key),最多一條記錄的鍵為null。用containsKey判斷是否存在鍵。可以使用Iterator進行遍歷。HashMap中hash陣列預設大小是16,而且一定是2的指數。HashMap 的例項有兩個引數影響其效能:“初始容量” 和 “載入因子”。容量 是雜湊表中桶的數量,初始容量 只是雜湊表在建立時的容量。載入因子 是雜湊表在其容量自動增加之前可以達到多滿的一種尺度預設載入因子是 0.75

,。當雜湊表中的條目數超出了載入因子與當前容量的乘積時,則要對該雜湊表進行 rehash 操作(即重建內部資料結構),從而雜湊表將具有大約兩倍的桶數。

雜湊表是由陣列+連結串列組成的,一個長度為16的陣列中,每個元素儲存的是一個連結串列的頭結點。那麼這些元素是按照什麼樣的規則儲存到陣列中呢。一般情況是通過hash(key)%len獲得,也就是元素的key的雜湊值對陣列長度取模得到。

2:存取操作

總的說:HashMap是基於hashing的原理,我們使用put(key, value)儲存物件到HashMap中,使用get(key)從HashMap中獲取物件。當我們給put()方法傳遞鍵和值時,我們先對鍵呼叫hashCode()方法,返回的hashCode用於找到bucket位置來儲存Entry物件。”這裡關鍵點在於指出,HashMap是在bucket中儲存鍵物件和值物件,作為Map.Entry。

2.1:向HashMap新增鍵值對時<key,value>時,需要經過如下步驟:

首先呼叫key的hashCode()方法生成一個hash值h1,如果不存在直接將<key,value>新增到hashMap中,如果已經存在找出所有HashMap中hash值為h1的key,然後分別呼叫key的equals()方法判斷當前新增的key值是否已經存在相同的key值。如果equals方法返回true說明當前需要新增的key已經存在,那麼使用新得value值覆蓋舊的value值。如果返回false說明新的key在HashMap不存在,那麼在HashMap中建立新的對映關係。

2.2:從hashMap中通過key查詢value時,需經歷步驟:
首先呼叫的是key的hashCode()方法判斷獲取 key的hash值h,這樣就可以確定鍵為key的所有值儲存的首地址。如果h對應的key值有多個那麼程式會遍歷所有key,通過key的equals()方法判斷key的內容是否相等,相等返回true時,對應的value是正確的結果。equals()方法比較規則:當引數obj引用的物件與當前物件為同一個物件時返回true。hashCode()返回物件的記憶體地址。

2.3:如果兩個鍵的hashcode相同,你如何獲取值物件?

因為hashcode相同,所以它們的bucket位置相同,‘碰撞’會發生。因為HashMap使用連結串列儲存物件,這個Entry(包含有鍵值對的Map.Entry物件)會儲存在連結串列中。找到bucket位置之後,會呼叫keys.equals()方法去找到連結串列中正確的節點,最終找到要找的值物件。

我們可以得出一個結論:如果兩個物件相等,那麼這兩個物件有著相同的hashCode。我們可以根據需要重寫hashCode和equals方法。

沒有重寫equals()的put

重寫equals()的put

3:當兩個物件的hashcode相同會發生什麼?   衝突

     3.1什麼是hash衝突?由於HashMap的雜湊桶的長度遠比hash取值範圍小,預設是16,所以當對hash值以桶的長度取餘,以找到存 放該key的桶的下標時,由於取餘是通過與操作完成的,會忽略hash值的高位。因此只有hashCode()的低位參加運算,發生不同的hash值,但是得到的index相同的情況的機率會大大增加,這種情況稱之為hash碰撞。 

     3.2什麼時候衝突?         當新增加的key的hash值已經在HasMap中存在時,就會產生衝突。

     3.3衝突處理辦法?          處理 hash衝突的辦法有開放地址法,再hash法。HashMap採用鏈地址法解決衝突。

     3.4避免衝突?使用不可變的宣告作final的物件,並採用合適的equals()和hashCode()方法的話,將會減少碰撞的發生提高效率。

     3.5 相比於之前的版本,jdk1.8在解決雜湊衝突時有了較大的變化,當連結串列長度大於閾值(預設為8)時,將連結串列轉化為紅黑樹,以減少搜尋時間。原本Map.Entry介面的實現類Entry改名為了Node。轉化為紅黑樹時改用另一種實現TreeNode。 

4:什麼是拉鍊法(鏈地址法)?

拉鍊法又叫鏈地址法,Java中的HashMap在儲存資料的時候就是用的拉鍊法來實現的,拉鍊發就是把具有相同雜湊地址的關鍵字(同義詞)值放在同一個單鏈表中。拉鍊法的工作原理:

HashMap<String, String> map = new HashMap<>(); map.put("K1", "V1"); map.put("K2", "V2"); map.put("K3", "V3");

  • 新建一個 HashMap,預設大小為 16;
  • 插入 <K1,V1> 鍵值對,先計算 K1 的 hashCode 為 115,使用除留餘數法得到所在的桶下標 115%16=3。
  • 插入 <K2,V2> 鍵值對,先計算 K2 的 hashCode 為 118,使用除留餘數法得到所在的桶下標 118%16=6。
  • 插入 <K3,V3> 鍵值對,先計算 K3 的 hashCode 為 118,使用除留餘數法得到所在的桶下標 118%16=6,插在 <K2,V2> 前面。

應該注意到連結串列的插入是以頭插法方式進行的,例如上面的 <K3,V3> 不是插在 <K2,V2> 後面,而是插入在連結串列頭部。

HashMap裡面用到鏈式資料結構的一個概念。上面我們提到過Entry類裡面有一個next屬性,作用是指向下一個Entry。打個比方, 第一個鍵值對A進來,通過計算其key的hash得到的index=0,記做:Entry[0] = A。一會後又進來一個鍵值對B,通過計算其index也等於0,現在怎麼辦?HashMap會這樣做:B.next = A,Entry[0] = B,如果又進來C,index也等於0,那麼C.next = B,Entry[0] = C;這樣我們發現index=0的地方其實存取了A,B,C三個鍵值對,他們通過next這個屬性連結在一起。所以疑問不用擔心。也就是說陣列中儲存的是最後插入的元素。

6:如果HashMap的大小超過了負載因子(load factor)定義的容量,怎麼辦?

 擴容是是新建了一個HashMap的底層陣列,而後呼叫transfer方法,將就HashMap的全部元素新增到新的HashMap中(要重新計算元素在新的陣列中的索引位置)。預設的負載因子大小為0.75,也就是說,當一個map填滿了75%的bucket時候,和其它集合類(如ArrayList等)一樣,將會建立原來HashMap大小的兩倍的bucket陣列,擴容前後,雜湊桶的長度一定會是2的次方,來重新調整map的大小,並將原來的物件放入新的bucket陣列中。這個過程叫作rehashing,因為它呼叫hash方法找到新的bucket位置。。

擴容時,如果發生過雜湊碰撞,節點數小於8個。則要根據連結串列上每個節點的雜湊值,依次放入新雜湊桶對應下標位置。 
因為擴容是容量翻倍,所以原連結串列上的每個節點,現在可能存放在原來的下標,即low位, 或者擴容後的下標,即high位。 high位= low位+原雜湊桶容量 
如果追加節點後,連結串列數量》=8,則轉化為紅黑樹

由迭代器的實現可以看出,遍歷HashMap時,順序是按照雜湊桶從低到高,連結串列從前往後,依次遍歷的。屬於無序集合。
程式碼:

void resize(int newCapacity) {   //傳入新的容量  
    Entry[] oldTable = table;    //引用擴容前的Entry陣列  
    int oldCapacity = oldTable.length;  
    if (oldCapacity == MAXIMUM_CAPACITY) {  //擴容前的陣列大小如果已經達到最大(2^30)了  
        threshold = Integer.MAX_VALUE; //修改閾值為int的最大值(2^31-1),這樣以後就不會擴容了  
        return;  
    }  

    Entry[] newTable = new Entry[newCapacity];  //初始化一個新的Entry陣列  
    transfer(newTable);                         //!!將資料轉移到新的Entry數組裡  
    table = newTable;                           //HashMap的table屬性引用新的Entry陣列  
    threshold = (int) (newCapacity * loadFactor);//修改閾值  
}  

void transfer(Entry[] newTable) {  
    Entry[] src = table;                   //src引用了舊的Entry陣列  
    int newCapacity = newTable.length;  
    for (int j = 0; j < src.length; j++) { //遍歷舊的Entry陣列  
        Entry<K, V> e = src[j];             //取得舊Entry陣列的每個元素  
        if (e != null) {  
            src[j] = null;//釋放舊Entry陣列的物件引用(for迴圈後,舊的Entry陣列不再引用任何物件)  
            do {  
                Entry<K, V> next = e.next;  
                int i = indexFor(e.hash, newCapacity); //!!重新計算每個元素在陣列中的位置  
                e.next = newTable[i]; //標記[1]  
                newTable[i] = e;      //將元素放在陣列上  
                e = next;             //訪問下一個Entry鏈上的元素  
            } while (e != null);  
        }  
    }  
}  

static int indexFor(int h, int length) {  
    return h & (length - 1);  
}  
--------------------- 程式碼來自網上的部落格

 

9 與HashTable的區別
與之相比HashTable是執行緒安全的,且不允許key、value是null。
HashTable預設容量是11。
HashTable是直接使用key的hashCode(key.hashCode())作為hash值,不像HashMap內部使用static final int hash(Object key)擾動函式對key的hashCode進行擾動後作為hash值。
HashTable取雜湊桶下標是直接用模運算%.(因為其預設容量也不是2的n次方。所以也無法用位運算替代模運算)
擴容時,新容量是原來的2倍+1。int newCapacity = (oldCapacity << 1) + 1;
Hashtable是Dictionary的子類同時也實現了Map介面,HashMap是Map介面的一個實現類;
 參考: https://blog.csdn.net/xx123698/article/details/60766072
原文:https://blog.csdn.net/zxt0601/article/details/77413921 
版權宣告:本文為博主原創文章,轉載請附上博文連結!