成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

什么?HashMap竟然也有懶加載?

開發 前端
前幾天H同學和我聊了下去谷歌的面試經驗,令我詫異的是,沒想到谷歌也問集合之后,我便覺得需要再整理一波集合相關的了。

[[417785]]

前幾天H同學和我聊了下去谷歌的面試經驗,令我詫異的是,沒想到谷歌也問集合之后,我便覺得需要再整理一波集合相關的了。

看文章前可以先看看以下高頻考點,如果覺得莫得問題,可以直接跳過該篇文章了,不用浪費時間。

  • new HashMap() 和 new HashMap(int initialCapacity) ,具體有什么區別?
  • HashMap中的數據多于多少個時才會進行擴容?
  • HashMap中的鏈表結構什么時候轉成紅黑樹?一定會轉嗎?
  • 紅黑樹結構又什么時候才會轉回鏈表?
  • 說說看HashMap的懶加載?負載因子的作用?

特征解析

為了搞清楚一個概念,這篇文章引入竹籃和雞蛋的概念,緩存的數據就是雞蛋,而節點就是竹籃,HashMap底層是一個竹籃數組,每個竹籃是一個鏈表或者紅黑樹的結構,每個竹籃也可以放多個雞蛋,因此其實可以當成二維數組來看待,只是在這里的第二維數組是一個鏈表或者紅黑樹,至于這個竹籃的結構是鏈表還是紅黑樹,就看竹籃內的的雞蛋個數有多少,并且鏈表和紅黑樹之間可以進行轉換。

通過散列函數將雞蛋定位到表中的具體竹籃,以提升查詢速度,其底層用于存放數據的數組也叫散列表。所謂散列函數,簡單來說就是將一個無限大的集合(在 HashMap 中,key值是一個無限大集合),經過 hash 運算取模,均勻的分布在一個有限的集合(我們定義的哈希表容量,比如長度 16 的數組)

源碼解析

成員變量和常量

  1. static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默認的初始容量,預計所有竹籃累計可以放16個雞蛋 
  2. static final int MAXIMUM_CAPACITY = 1 << 30; // 最大容量,所有竹籃最多可以放多少個雞蛋 
  3. static final float DEFAULT_LOAD_FACTOR = 0.75f; // 默認加載因子,裝逼用,一般都會在應用探討bb的時候提一嘴 
  4. static final int TREEIFY_THRESHOLD = 8;  // 單個竹籃放的雞蛋個數超過8轉紅黑樹 
  5. static final int UNTREEIFY_THRESHOLD = 6; // 單個竹籃放的雞蛋個數小于6則轉鏈表 
  6. static final int MIN_TREEIFY_CAPACITY = 64; // 如果竹籃個數小于64個,會先進行擴容,而不會鏈表轉紅黑樹 
  7.  
  8. transient Node<K,V>[] table;  // 哈希表數組,存儲數據的地方,每個竹籃結構可能為鏈表或者紅黑樹,總是2的冪次倍 
  9. transient Set<Map.Entry<K,V>> entrySet; // 存放具體元素的集合,可用于遍歷Map 
  10. transient int size; // 總的雞蛋個數 
  11. transient int modCount;  // 插入刪除元素,modCount++,用于記錄改變次數 
  12. int threshold; // 容量閾值,所有竹籃允許緩存雞蛋的個數,超過這個值需要擴容,默認為0,看看后續是如何變化的,對理解擴容那塊很重要 
  13. final float loadFactor; // 加載因子,能夠權衡時間復雜度和空間復雜度 

筆試or面試需要記住幾個死記硬背的點,那就是:hashmap的默認初始容量為16,加載因子是0.75,鏈表長過8則轉紅黑樹,小于6則轉鏈表,容量低于64則先擴容,而不會鏈表轉紅黑樹。

看看構造方法

HashMap有四個構造方法,這里只列出我們常用的兩個

  • 第一個是默認構造方法,我們不指定默認所有竹籃累計可以放16個雞蛋
  • 第二個是指定初始容量,也是我們比較推薦的做法,根據需要設置大小,避免后面resize擴容開銷

日常開發中如果知道大小,我們都會用第二種,可以避免resize擴容開銷,新手開發經常會直接用第一種,一般我review代碼看到都會直接打回改,然后告訴他們為啥,想想看,如果你已經知道這個map容器要裝的元素是100個,你還不指定初始容量,那么就會導致在第一次put數據的時候進行擴容,此時第一次擴容因為沒有初始容量,計算出的容量閾值為默認大小16,16*加載因子0.75f就是12,之后當你繼續put值超過12個的時候又會繼續擴容,第二次擴容后容量閾值為24,循環剛剛的過程直到容量閾值大于100,而如果指定了默認大小則第一次擴容就夠用了,不用走后面那么多次擴容,可以節省性能;

其次是命名也要注意下,最好是直接用Map結合,比如xxxMap,這是規范。

擴容

我們先回顧下上面的一個成員變量threshold

「int threshold; // 容量閾值,所有竹籃允許放入雞蛋的個數」

ok了,繼續講擴容,擴容分為多種情況

  • 如果我們使用了無參構造方法,可以看到
圖片

其中并未對成員變量threshold容量閾值進行初始化,反調threshold賦值的地方可以看到在put第一個元素的時候會調用resize方法,該方法有做了容量閾值的計算,計算方式為:DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY,也就是默認的加載因子 * 默認容量,即 0.75f * 16,即12

  • 如果指定了初始容量
圖片
圖片

可以看到初始容量閾值的計算公式是:

圖片

這么一坨東西是啥呢,其實就是一個算法,看不懂也莫得關系,畢竟我也看不懂,記住就好,這個算法其實就是返回大于輸入參數且最近的2的整數次冪的數,比如設置的初始容量為10,那么這個算法則返回16。

但是,到了這里其實也并沒有分配好數組,可以看到resize方法,其實也是在put的時候才會真正進行數組的分配,也就是懶加載了。

  • 二次擴容,最終都會走向resize方法,每次擴容量都會是原先的2倍。

我們可以看看resize方法

  1. final Node<K,V>[] resize() { 
  2.         Node<K,V>[] oldTab = table
  3.         int oldCap = (oldTab == null) ? 0 : oldTab.length; 
  4.         int oldThr = threshold; 
  5.         int newCap, newThr = 0; 
  6.         if (oldCap > 0) { 
  7.             // 如果竹籃個數大于最大容量,則將容量閾值設置成int的最大值 
  8.             if (oldCap >= MAXIMUM_CAPACITY) { 
  9.                 threshold = Integer.MAX_VALUE; 
  10.                 return oldTab; 
  11.             } 
  12.            // 否則新閾值為舊閾值兩倍 
  13.             else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && 
  14.                      oldCap >= DEFAULT_INITIAL_CAPACITY) 
  15.                 newThr = oldThr << 1; 
  16.         } 
  17.         else if (oldThr > 0)  
  18.             newCap = oldThr; 
  19.         else {               
  20.            // 只有默認無參構造方法才會走到這個分支,閾值為默認算法,容量 * 0.75 
  21.             newCap = DEFAULT_INITIAL_CAPACITY; 
  22.             newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); 
  23.         } 
  24.         if (newThr == 0) { 
  25.             float ft = (float)newCap * loadFactor; 
  26.             newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? 
  27.                       (int)ft : Integer.MAX_VALUE); 
  28.         } 
  29.         threshold = newThr; 
  30.         @SuppressWarnings({"rawtypes","unchecked"}) 
  31.     // 真正進行擴容的地方,也是凌亂的算法我大致講講,首先要創建一個新的哈希表,其容量為上面計算出來的 
  32.         Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; 
  33.         table = newTab; 
  34.         if (oldTab != null) { 
  35.         // 輪詢操作,對所有元素重哈希 
  36.         for (int j = 0; j < oldCap; ++j) { 
  37.             Node<K,V> e; 
  38.             if ((e = oldTab[j]) != null) { 
  39.                 oldTab[j] = null
  40.                 if (e.next == null
  41.                     newTab[e.hash & (newCap - 1)] = e; 
  42.                 else if (e instanceof TreeNode) 
  43.                     // 竹籃內內的雞蛋數據重hash,紅黑樹轉鏈表的地方,內部處理主要是當竹籃內的雞蛋個數小于等于6時,樹結構會還原成鏈表 
  44.                     ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); 
  45.                 else { // preserve order 
  46.                     // 略:鏈表元素重hash 
  47.                 } 
  48.             } 
  49.         } 
  50.     } 
  51.     return newTab; 

HashMap擴容可以分為三種:

  • 使用默認構造方法初始化HashMap,從前文可以知道HashMap在一開始初始化的時候thershold容量閾值為0,默認值DEFAULT_INITIAL_CAPACITY也就是16,DEFAULT_LOAD_FACTOR為0.75f,因此在第一次put數據的時候會進行擴容,擴容后的容量閾值threshold為DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR = 12;
  • 指定初始容量的構造方法初始化HashMap,可以看到在初始化的時候便通過tableSizeFor進行計算,也就是返回大于輸入參數且最近的2的整數次冪的數;
  • 當HashMap不是第一次擴容的時候,那么每次擴容的容量以及容量閾值threshold為原有的兩倍。

hash計算

圖片

HashCode是啥其實大家都知道,無非就是用來確定對象在HashMap中的存儲地址,目的也很簡單,為了快,貌似除了了那方面,其他都是越快越好,比如賺錢。

元素插入

我們先回顧下上面的一個成員變量table

「itransient Node<K,V>[] table; // 哈希表數組,存儲數據的地方,每個竹籃結構可能為鏈表或者紅黑樹,總是2的冪次倍」

  1.   
  2. final V putVal(int hash, K key, V value, boolean onlyIfAbsent, 
  3.                 boolean evict) { 
  4.     Node<K,V>[] tab; Node<K,V> p; int n, i; 
  5.     // 這里如果發現動態數組為null則會初始化數組。 
  6.     if ((tab = table) == null || (n = tab.length) == 0) 
  7.         // 第一次放入值時會在這里初始化數組,并且通過resize方法進行擴容 
  8.         n = (tab = resize()).length; 
  9.     // 通過hash發現要放入的雞蛋的數組位置為null,說明沒有hash沖突,則直接把該雞蛋放在這里即可 
  10.     if ((p = tab[i = (n - 1) & hash]) == null
  11.         tab[i] = newNode(hash, key, value, null); 
  12.     else { 
  13.         // 如果要放入的位置已經有該雞蛋了 
  14.         Node<K,V> e; K k; 
  15.         // 判斷竹籃的第一個雞蛋否和新元素key以及hash值都完全一致,如果是則不用看代碼都知道進行覆蓋,覆蓋的邏輯在后面 
  16.         if (p.hash == hash && 
  17.             ((k = p.key) == key || (key != null && key.equals(k)))) 
  18.             e = p; 
  19.         // 確認是否為樹解點 
  20.         else if (p instanceof TreeNode) 
  21.             // 如果是的話則按照紅黑樹方法放入竹籃內 
  22.             e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); 
  23.         else { 
  24.             // 說明不是,則是列表,按照列表方法放入 
  25.             for (int binCount = 0; ; ++binCount) { 
  26.                 // 一直向下取,直到找到空的位置 
  27.                 if ((e = p.next) == null) { 
  28.                     p.next = newNode(hash, key, value, null); 
  29.                     //  判斷鏈表長度是否大于8,這里其實減后為7,判斷的是binCount,但是因為插入了一個新節點了,所以其實為8 
  30.                     if (binCount >= TREEIFY_THRESHOLD - 1)  
  31.                         // 則將列表轉為紅黑樹,所以記住大于8的時候會轉成紅黑樹 
  32.                         treeifyBin(tab, hash); 
  33.                     break; 
  34.                 } 
  35.                 // 已經找到了hash值和key一樣的,則直接break,不用找了,同樣在后面進行覆蓋 
  36.                 if (e.hash == hash && 
  37.                     ((k = e.key) == key || (key != null && key.equals(k)))) 
  38.                     break; 
  39.                 p = e; 
  40.             } 
  41.         } 
  42.  
  43.         // 覆蓋操作在這里,新值和舊值的key完全相同,進行覆蓋操作 
  44.         if (e != null) {  
  45.             V oldValue = e.value; 
  46.             if (!onlyIfAbsent || oldValue == null
  47.                 e.value = value; 
  48.             // 訪問后回調 
  49.             afterNodeAccess(e); 
  50.             return oldValue; 
  51.         } 
  52.     } 
  53.     ++modCount; 
  54.     // 當map中所有的雞蛋個數大于容量閾值時則進行擴容,二次擴容如上面所說,也就是兩倍 
  55.     if (++size > threshold) 
  56.         resize(); 
  57.     afterNodeInsertion(evict); 
  58.     return null
  1.   
  2. final V putVal(int hash, K key, V value, boolean onlyIfAbsent, 
  3.                 boolean evict) { 
  4.     Node<K,V>[] tab; Node<K,V> p; int n, i; 
  5.     // 這里如果發現動態數組為null則會初始化數組。 
  6.     if ((tab = table) == null || (n = tab.length) == 0) 
  7.         // 第一次放入值時會在這里初始化數組,并且通過resize方法進行擴容 
  8.         n = (tab = resize()).length; 
  9.     // 通過hash發現要放入的雞蛋的數組位置為null,說明沒有hash沖突,則直接把該雞蛋放在這里即可 
  10.     if ((p = tab[i = (n - 1) & hash]) == null
  11.         tab[i] = newNode(hash, key, value, null); 
  12.     else { 
  13.         // 如果要放入的位置已經有該雞蛋了 
  14.         Node<K,V> e; K k; 
  15.         // 判斷竹籃的第一個雞蛋否和新元素key以及hash值都完全一致,如果是則不用看代碼都知道進行覆蓋,覆蓋的邏輯在后面 
  16.         if (p.hash == hash && 
  17.             ((k = p.key) == key || (key != null && key.equals(k)))) 
  18.             e = p; 
  19.         // 確認是否為樹解點 
  20.         else if (p instanceof TreeNode) 
  21.             // 如果是的話則按照紅黑樹方法放入竹籃內 
  22.             e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); 
  23.         else { 
  24.             // 說明不是,則是列表,按照列表方法放入 
  25.             for (int binCount = 0; ; ++binCount) { 
  26.                 // 一直向下取,直到找到空的位置 
  27.                 if ((e = p.next) == null) { 
  28.                     p.next = newNode(hash, key, value, null); 
  29.                     //  判斷鏈表長度是否大于8,這里其實減后為7,判斷的是binCount,但是因為插入了一個新節點了,所以其實為8 
  30.                     if (binCount >= TREEIFY_THRESHOLD - 1)  
  31.                         // 則將列表轉為紅黑樹,所以記住大于8的時候會轉成紅黑樹 
  32.                         treeifyBin(tab, hash); 
  33.                     break; 
  34.                 } 
  35.                 // 已經找到了hash值和key一樣的,則直接break,不用找了,同樣在后面進行覆蓋 
  36.                 if (e.hash == hash && 
  37.                     ((k = e.key) == key || (key != null && key.equals(k)))) 
  38.                     break; 
  39.                 p = e; 
  40.             } 
  41.         } 
  42.  
  43.         // 覆蓋操作在這里,新值和舊值的key完全相同,進行覆蓋操作 
  44.         if (e != null) {  
  45.             V oldValue = e.value; 
  46.             if (!onlyIfAbsent || oldValue == null
  47.                 e.value = value; 
  48.             // 訪問后回調 
  49.             afterNodeAccess(e); 
  50.             return oldValue; 
  51.         } 
  52.     } 
  53.     ++modCount; 
  54.     // 當map中所有的雞蛋個數大于容量閾值時則進行擴容,二次擴容如上面所說,也就是兩倍 
  55.     if (++size > threshold) 
  56.         resize(); 
  57.     afterNodeInsertion(evict); 
  58.     return null
  1. final void treeifyBin(Node<K,V>[] tab, int hash) { 
  2.     int n, index; Node<K,V> e; 
  3.     // 當tab為空或者竹籃個數小于64個,會先進行擴容,而不會鏈表轉紅黑樹 
  4.     if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) 
  5.         resize(); 
  6.     else if ((e = tab[index = (n - 1) & hash]) != null) { 
  7.         // 真正進行轉換的地方 
  8.         TreeNode<K,V> hd = null, tl = null
  9.         do { 
  10.             TreeNode<K,V> p = replacementTreeNode(e, null); 
  11.             if (tl == null
  12.                 hd = p; 
  13.             else { 
  14.                 p.prev = tl; 
  15.                 tl.next = p; 
  16.             } 
  17.             tl = p; 
  18.         } while ((e = e.next) != null); 
  19.         if ((tab[index] = hd) != null
  20.             hd.treeify(tab); 
  21.     } 

put總體流程匯總如下:

圖片

羅列幾個該注意的點,分別是:

  • HashMap中緩存數據的數組table,我們可以看到初始化的時候默認是null,是在第一次put數據的時候才進行初始化的,這也是所謂的懶加載,記住了,不要每次提HashMap的懶加載機制都二臉懵逼了。
  • HashMap中單個竹籃存放的雞蛋個數大于8,并且當竹籃個數大于64個的時候則將列表轉為紅黑樹,否則進行擴容。
  • 每次put數據時當map中存放的所有雞蛋個數大于容量閾值時則進行擴容,并且是先put數據,再擴容。

元素查詢

  1. final Node<K,V> getNode(int hash, Object key) { 
  2.     Node<K,V>[] tab; Node<K,V> first, e; int n; K k; 
  3.     if ((tab = table) != null && (n = tab.length) > 0 && 
  4.         (first = tab[(n - 1) & hash]) != null) { 
  5.        // 如果計算hash值和第一個節點的key值相同,直接返回 
  6.         if (first.hash == hash && // always check first node 
  7.             ((k = first.key) == key || (key != null && key.equals(k)))) 
  8.             return first
  9.         if ((e = first.next) != null) { 
  10.            //如果為紅黑樹節點,以紅黑樹方式查找 
  11.             if (first instanceof TreeNode) 
  12.                 return ((TreeNode<K,V>)first).getTreeNode(hash, key); 
  13.             do { 
  14.                //如果hash值相同,key不同,則遍歷鏈表找到相同的key,返回 
  15.                 if (e.hash == hash && 
  16.                     ((k = e.key) == key || (key != null && key.equals(k)))) 
  17.                     return e; 
  18.             } while ((e = e.next) != null); 
  19.         } 
  20.     } 
  21.     return null

查找這里,總結下來流程便是:

  • 根據hash值從竹籃數組內找到竹籃,判斷頭節點key值與當前key相同,直接返回
  • 如果該竹籃為TreeNode節點,以紅黑樹方式查找
  • 如果不是則循環遍歷鏈表,直到查到對應的key相同

元素刪除

  1. final Node<K,V> removeNode(int hash, Object key, Object value, 
  2.                                boolean matchValue, boolean movable) { 
  3.         Node<K,V>[] tab; Node<K,V> p; int n, index
  4.         if ((tab = table) != null && (n = tab.length) > 0 && 
  5.             (p = tab[index = (n - 1) & hash]) != null) { 
  6.             Node<K,V> node = null, e; K k; V v; 
  7.             if (p.hash == hash && 
  8.                 ((k = p.key) == key || (key != null && key.equals(k)))) 
  9.                 node = p; 
  10.             else if ((e = p.next) != null) { 
  11.                //紅黑樹的查找方式 
  12.                 if (p instanceof TreeNode) 
  13.                     node = ((TreeNode<K,V>)p).getTreeNode(hash, key); 
  14.                 else { 
  15.                    //鏈表遍歷查找方式 
  16.                     do { 
  17.                         if (e.hash == hash && 
  18.                             ((k = e.key) == key || 
  19.                              (key != null && key.equals(k)))) { 
  20.                             node = e; 
  21.                             break; 
  22.                         } 
  23.                         p = e; 
  24.                     } while ((e = e.next) != null); 
  25.                 } 
  26.             } 
  27.            //刪除node 
  28.             if (node != null && (!matchValue || (v = node.value) == value || 
  29.                                  (value != null && value.equals(v)))) { 
  30.                //如果node為紅黑樹結點,采用紅黑樹刪除方式 
  31.                 if (node instanceof TreeNode) 
  32.                     ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable); 
  33.                // 如果是鏈表并且node為頭結點,當前數組下標元素直接替換為next 
  34.                 else if (node == p) 
  35.                     tab[index] = node.next
  36.                 else 
  37.                    //鏈表非頭元素刪除方式 
  38.                     p.next = node.next
  39.                 ++modCount; 
  40.                 --size; 
  41.                 afterNodeRemoval(node); 
  42.                 return node; 
  43.             } 
  44.         } 
  45.         return null
  46.     } 

刪除操作很簡單,先根據hash找到對應雞蛋,然后根據不同類型的節點進行刪除,沒什么注意的點,如果有,那就是如果是鏈表表頭的話,則需要將下一個節點賦值為表頭。

本文轉載自微信公眾號「稀飯下雪」,可以通過以下二維碼關注。轉載本文請聯系稀飯下雪公眾號。

 

責任編輯:姜華 來源: 稀飯下雪
相關推薦

2020-11-18 09:30:29

圖片懶加載前端瀏覽器

2011-01-17 19:35:04

javascriptjqueryweb

2015-10-08 10:58:51

圖片懶加載

2017-03-28 10:11:12

Webpack 2React加載

2020-06-01 08:04:18

三目運算符代碼

2015-07-20 15:26:56

WiFi感知

2022-06-07 08:18:49

懶加載Web前端

2021-03-19 06:31:06

vue-lazyloa圖片懶加載項目

2020-08-19 16:36:53

HashMap紅黑樹閾值

2020-12-31 07:57:25

JVM操作代碼

2022-04-28 08:52:40

懶加載Web

2019-09-09 09:05:59

圖片框架懶加載

2018-08-02 14:08:47

小程序javascriptlazyload

2022-08-31 10:40:40

MySQL數據庫

2015-11-23 14:29:16

流量提速降費運營商

2020-04-22 20:35:02

HashMap線程安全

2021-09-10 06:50:03

HashMapHash方法

2024-03-20 09:31:00

圖片懶加載性能優化React

2024-01-08 08:50:19

Vue3級聯菜單數據懶加載

2020-05-27 12:45:52

HashMapJava加載因子
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 久久精品欧美一区二区三区不卡 | 国产成人jvid在线播放 | 日韩一级免费电影 | 老牛影视av一区二区在线观看 | 91麻豆精品国产91久久久更新资源速度超快 | 亚洲高清中文字幕 | 成人激情视频在线 | 成人黄色a| 国产区精品在线观看 | 伊人久久免费 | 一级黄大片 | 北条麻妃99精品青青久久 | 99成人免费视频 | 久久久久久国产精品 | www亚洲精品| 国产精品欧美精品 | 久久精品中文字幕 | 日韩视频在线免费观看 | 成人欧美一区二区三区在线播放 | 黄色一级特级片 | 午夜视频网 | 在线一区视频 | 91九色网站| 91av在线免费播放 | 精品一二三 | 欧美精品一区二区三区在线四季 | 蜜臀av日日欢夜夜爽一区 | 日韩一级免费 | 一区二区日本 | 少妇特黄a一区二区三区88av | 国产一区二区自拍 | 国产精品视频久久久久久 | 午夜影院在线观看视频 | 精品久久久久香蕉网 | 全免一级毛片 | re久久 | 91在线精品视频 | 免费av电影网站 | 欧美精品在欧美一区二区少妇 | 久久er精品 | 日韩精品一区二区三区中文字幕 |