面試需要了解的HashMap

java面試中,HashMap集合的面試基本是都會問到的。

哈希表(hash table)
也叫散列表,是一種非常重要的數據結構,應用場景及其豐富,許多緩存技術(比如memcached)的核心其實就是在內存中維護一張大的哈希表。

Map實現類

Java為數據結構中的映射定義了一個接口Map,主要有四個常用的實現類,分別是HashMap、Hashtable、LinkedHashMap和TreeMap。

針對這4個實現類做個簡單的說明:

(1) HashMap:它根據鍵的hashCode值存儲數據,大多數情況下可以直接定位到它的值,因而具有很快的訪問速度,但遍歷順序卻是不確定的。 HashMap最多隻允許一條記錄的鍵為null,允許多條記錄的鍵值為null。HashMap非線程安全,即任一時刻可以有多個線程同時寫HashMap,可能會導致數據的不一致。如果需要滿足線程安全,通常使用ConcurrentHashMap代替。

(2) Hashtable:Hashtable是遺留類,很多映射的常用功能與HashMap類似,不同的是它承自Dictionary類,並且是線程安全的(方法上使用悲觀鎖synchronized),任一時間只有一個線程能寫Hashtable,併發性不如ConcurrentHashMap,因為ConcurrentHashMap引入了分段鎖。Hashtable不建議使用,不需要線程安全的場合可以用HashMap替換,需要線程安全的場合可以用ConcurrentHashMap替換。

(3) LinkedHashMap:LinkedHashMap是HashMap的一個子類,保存了記錄的插入順序,在用Iterator遍歷LinkedHashMap時,先得到的記錄肯定是先插入的,也可以在構造時帶參數,按照訪問次序排序。

(4) TreeMap:TreeMap實現SortedMap接口,能夠把它保存的記錄根據鍵排序,默認是按鍵值的升序排序。如果使用排序的映射,建議使用TreeMap。在使用TreeMap時,key必須實現Comparable接口或者在構造TreeMap傳入自定義的Comparator,否則會拋出ClassCastException類型的異常。

存儲結構:jdk1.7是數組+鏈表,jdk1.8是數組+鏈表+紅黑樹。


面試需要了解的HashMap

hashmap底層結構


鏈表主要為了解決哈希衝突(也叫哈希碰撞)而存在的。主要是因為使用hashmap存儲數據的時候,兩個不同的元素(鍵值),通過哈希函數得出的實際存儲地址相同。哈希衝突的解決方案有多種:開放定址法(發生衝突,繼續尋找下一塊未被佔用的存儲地址),再散列函數法,鏈地址法,而HashMap就是採用了鏈地址法。

JDK 1.8 對 HashMap 進行了比較大的優化,底層實現由之前的 “數組+鏈表” 改為 “數組+鏈表+紅黑樹”,當鏈表節點較少時仍然是以鏈表存在,當鏈表節點較多時(等於8,將會調用內部的treeifyBin方法)轉為紅黑樹。

JDK 1.8數組使用了Node,它實現了Map.Entry接口,本質是就是一個映射(鍵值對)。

初始化幾個重要的參數:

<code>int threshold;             // 所能容納的key-value對極限 
final float loadFactor;    // 負載因子
int modCount;  //修改的次數,用於fail-fast容錯
int size; //數組大小/<code>

Node[] table的初始化數組大小(length )默認值是16,負載因子(loadFactor)默認值是0.75,threshold是HashMap所能容納的最大數據量的Node(鍵值對)個數。threshold = length * loadFactor。得出負載因子越大,所能容納的鍵值對個數就越多。

put描述過程(查看源碼更清晰)

1、判斷數組是否為空,為空進行初始化,不為空,計算 k 的 hash 值,通過(n - 1) & hash計算應當存放在數組中的下標 index。

2、查看 table[index] 是否存在數據,沒有數據就構造一個Node節點存放在 table[index] 中。存在數據,說明發生了hash衝突(存在二個節點key的hash值一樣), 繼續判斷key是否相等,相等,用新的value替換原數據(onlyIfAbsent為false),如果不相等,判斷當前節點類型是不是樹型節點,如果是樹型節點,創造樹型節點插入紅黑樹中;

3、如果不是樹型節點,創建普通Node加入鏈表中;判斷鏈表長度是否大於 8,大於的話鏈表轉換為紅黑樹;

4、插入完成之後判斷當前節點數是否大於閾值,如果大於開始擴容為原數組的二倍

主要代碼實現:

<code>public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
 
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node[] tab; Node p; int n, i;
    // 1.校驗table是否為空或者length等於0,如果是則調用resize方法進行初始化
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 2.通過hash值計算索引位置,將該索引位置的頭節點賦值給p,如果p為空則直接在該索引位置新增一個節點即可
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        // table表該索引位置不為空,則進行查找
        Node e; K k;
        // 3.判斷p節點的key和hash值是否跟傳入的相等,如果相等, 則p節點即為要查找的目標節點,將p節點賦值給e節點
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // 4.判斷p節點是否為TreeNode, 如果是則調用紅黑樹的putTreeVal方法查找目標節點
        else if (p instanceof TreeNode)
            e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
        else {
            // 5.走到這代表p節點為普通鏈表節點,則調用普通的鏈表方法進行查找,使用binCount統計鏈表的節點數
            for (int binCount = 0; ; ++binCount) {
                // 6.如果p的next節點為空時,則代表找不到目標節點,則新增一個節點並插入鏈表尾部
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // 7.校驗節點數是否超過8個,如果超過則調用treeifyBin方法將鏈表節點轉為紅黑樹節點,
                    // 減一是因為循環是從p節點的下一個節點開始的
                    if (binCount >= TREEIFY_THRESHOLD - 1)
                        treeifyBin(tab, hash);
                    break;
                }
                // 8.如果e節點存在hash值和key值都與傳入的相同,則e節點即為目標節點,跳出循環
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;  // 將p指向下一個節點
            }
        }
        // 9.如果e節點不為空,則代表目標節點存在,使用傳入的value覆蓋該節點的value,並返回oldValue
        if (e != null) {
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e); // 用於LinkedHashMap
            return oldValue;
        }
    }
    ++modCount;
    // 10.如果插入節點後節點數超過閾值,則調用resize方法進行擴容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);  // 用於LinkedHashMap
    return null;
}/<code> 

resize擴容機制

兩個重要屬性:

<code>Capacity:HashMap當前長度。
LoadFactor:負載因子,默認值0.75f。/<code>

主要兩個步驟:

1、創建一個新的Entry空數組,長度是原數組的2倍。

2、遍歷原Entry數組,把所有的Entry重新Hash到新數組。

為啥要重新Hash!!!臥槽這個問題!有點知識盲區呀!有沒有很懵逼!直接複製過去不香麼

這裡我們回想下計算index的公式:index = HashCode(Key) & (Length - 1),原來長度(Length)是8你位運算出來的值是2 ,新的長度是16你位運算出來的值明顯不一樣了。

代碼全部貼出來著實有點噁心,就分幾個步驟說了:

一、擴容:如果超過了數組的最大容量,那麼就直接將閾值設置為整數最大值,然後如果沒有超過,那就擴容為原來的2倍。

<code>if (oldCap > 0) {
      if (oldCap >= MAXIMUM_CAPACITY) {
           threshold = Integer.MAX_VALUE;
           return oldTab;
      }
      else if ((newCap = oldCap < 
< 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; }/<code>

二、設置閾值:如果閾值(oldThr)已經初始化過了,新閾值(newCap)直接使用舊的閾值。如果沒有初始化,那就初始化一個新的數組容量和新的閾值。最後為當前的容量閾值賦值。

<code>//第二部分:設置閾值
else if (oldThr > 0)
      newCap = oldThr;
else {    // 沒有初始化閾值那就初始化一個默認的容量和閾值
      newCap = DEFAULT_INITIAL_CAPACITY;
      newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
      float ft = (float)newCap * loadFactor;
      newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
}
//為當前的容量閾值賦值
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node[] newTab = (Node[])new Node[newCap];
table = newTab;/<code>

三、複製數組到新數組(這個代碼就不貼了,大家有興趣自己去研究下)


分享到:


相關文章: