<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          今日面試之HashMap考點(diǎn)

          共 15392字,需瀏覽 31分鐘

           ·

          2021-04-26 07:46

          點(diǎn)擊關(guān)注,與你共同成長(zhǎng)!



          關(guān)于HashMap常見面試考點(diǎn)(底層原理+擴(kuò)容機(jī)制)

          問:簡(jiǎn)單說說 HashMap 的底層原理?

          答:當(dāng)我們往 HashMap 中 put 元素時(shí),先根據(jù) key 的 hash 值得到這個(gè) Entry 元素在數(shù)組中的位置(即下標(biāo)),然后把這個(gè) Entry 元素放到對(duì)應(yīng)的位置中,如果這個(gè) Entry 元素所在的位子上已經(jīng)存放有其他元素就在同一個(gè)位子上的 Entry 元素以鏈表的形式存放,新加入的放在鏈頭,從 HashMap 中 get  Entry 元素時(shí)先計(jì)算 key 的 hashcode,找到數(shù)組中對(duì)應(yīng)位置的某一 Entry 元素,然后通過 key 的 equals 方法在對(duì)應(yīng)位置的鏈表中找到需要的 Entry 元素,所以 HashMap 的數(shù)據(jù)結(jié)構(gòu)是數(shù)組和鏈表的結(jié)合,此外 HashMap 中 key 和 value 都允許為 null,key 為 null 的鍵值對(duì)永遠(yuǎn)都放在以 table[0] 為頭結(jié)點(diǎn)的鏈表中。

          之所以 HashMap 這么設(shè)計(jì)的實(shí)質(zhì)是由于數(shù)組存儲(chǔ)區(qū)間是連續(xù)的,占用內(nèi)存嚴(yán)重,故空間復(fù)雜度大,但二分查找時(shí)間復(fù)雜度?。∣(1)),所以尋址容易而插入和刪除困難;而鏈表存儲(chǔ)區(qū)間離散,占用內(nèi)存比較寬松,故空間復(fù)雜度小,但時(shí)間復(fù)雜度大(O(N)),所以尋址困難而插入和刪除容易;所以就產(chǎn)生了一種新的數(shù)據(jù)結(jié)構(gòu)叫做哈希表,哈希表既滿足數(shù)據(jù)的查找方便,同時(shí)不占用太多的內(nèi)容空間,使用也十分方便,哈希表有多種不同的實(shí)現(xiàn)方法,HashMap 采用的是鏈表的數(shù)組實(shí)現(xiàn)方式。

          特別說明,對(duì)于 JDK 1.8 開始 HashMap 實(shí)現(xiàn)原理變成了數(shù)組+鏈表+紅黑樹的結(jié)構(gòu),數(shù)組鏈表部分基本不變,紅黑樹是為了解決哈希碰撞后鏈表索引效率的問題,所以在 JDK 1.8 中當(dāng)鏈表的節(jié)點(diǎn)大于 8 個(gè)時(shí)就會(huì)將鏈表變?yōu)榧t黑樹。區(qū)別是 JDK 1.8 以前碰撞節(jié)點(diǎn)會(huì)在鏈表頭部插入,而 JDK 1.8 開始碰撞節(jié)點(diǎn)會(huì)在鏈表尾部插入,對(duì)于擴(kuò)容操作后的節(jié)點(diǎn)轉(zhuǎn)移 JDK 1.8 以前轉(zhuǎn)移前后鏈表順序會(huì)倒置,而 JDK 1.8 中依然保持原序。

          問:HashMap 默認(rèn)的初始化長(zhǎng)度是多少?為什么默認(rèn)長(zhǎng)度和擴(kuò)容后的長(zhǎng)度都必須是 2 的冪?

          答:在 JDK 中默認(rèn)長(zhǎng)度是 16(在 Android SDK 中的 HashMap 默認(rèn)長(zhǎng)度為 4),并且默認(rèn)長(zhǎng)度和擴(kuò)容后的長(zhǎng)度都必須是 2 的冪。因?yàn)槲覀兛梢韵瓤聪?HashMap 的 put 方法核心,如下:

          public V put(K key, V value) {
           
             ......
           
             //計(jì)算出 key 的 hash 值
           
             int hash = hash(key);
           
             //通過 key 的 hash 值和當(dāng)前動(dòng)態(tài)數(shù)組的長(zhǎng)度求當(dāng)前 key 的 Entry 在數(shù)組中的下標(biāo)
           
             int i = indexFor(hash, table.length);
           
             ......
           
          }
           
           
          //最核心的求數(shù)組下標(biāo)方法
           
          static int indexFor(int h, int length) {
           
             // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
           
             return h & (length-1);
           
          }

          可以看到獲取數(shù)組索引的計(jì)算方式為 key 的 hash 值按位與運(yùn)算數(shù)組長(zhǎng)度減一,為了說明長(zhǎng)度盡量是 2 的冪的作用我們假設(shè)執(zhí)行了 put("android", 123); 語(yǔ)句且 "android" 的 hash 值為 234567,二進(jìn)制為 111001010001000111,然后由于 HashMap 默認(rèn)長(zhǎng)度為 16,所以減一后的二進(jìn)制為 1111,接著兩數(shù)做按位與操作二進(jìn)制結(jié)果為 111,即十進(jìn)制的 7,所以 index 為 7,可以看出這種按位操作比直接取模效率要高。

          如果假設(shè) HashMap 默認(rèn)長(zhǎng)度不是 2 的冪,譬如數(shù)組長(zhǎng)度為 6,減一的二進(jìn)制為 101,與 111001010001000111 按位與操作二進(jìn)制 101,此時(shí)假設(shè)我們通過 put 再放一個(gè) key-value 進(jìn)來,其 hash 為 111001010001000101,其與 101 按位與操作后的二進(jìn)制也為 101,很容易發(fā)生哈希碰撞,這就不符合 index 的均勻分布了。

          通過上面兩個(gè)假設(shè)例子可以看出 HashMap 的長(zhǎng)度為 2 的冪時(shí)減一的值的二進(jìn)制位數(shù)一定全為 1,這樣數(shù)組下標(biāo) index 的值完全取決于 key 的 hash 值的后幾位,因此只要存入 HashMap 的 Entry 的 key 的 hashCode 值分布均勻,HashMap 中數(shù)組 Entry 元素的分部也就盡可能是均勻的(也就避免了 hash 碰撞帶來的性能問題),所以當(dāng)長(zhǎng)度為 2 的冪時(shí)不同的 hash 值發(fā)生碰撞的概率比較小,這樣就會(huì)使得數(shù)據(jù)在 table 數(shù)組中分布較均勻,查詢速度也較快。不過即使負(fù)載因子和 hash 算法設(shè)計(jì)的再合理也免不了哈希沖突碰撞的情況,一旦出現(xiàn)過多就會(huì)影響 HashMap 的性能,所以在 JDK 1.8 中官方對(duì)數(shù)據(jù)結(jié)構(gòu)引入了紅黑樹,當(dāng)鏈表長(zhǎng)度太長(zhǎng)(默認(rèn)超過 8)時(shí)鏈表就轉(zhuǎn)為了紅黑樹,而紅黑樹的增刪改查都比較高效,從而解決了哈希碰撞帶來的性能問題。

          問:簡(jiǎn)單說說你對(duì) HashMap 構(gòu)造方法中 initialCapacity(初始容量)、loadFactor(加載因子)的理解?

          答:這兩個(gè)參數(shù)對(duì)于 HashMap 來說很重要,直接從一定程度決定了 HashMap 的性能問題。

          initialCapacity 初始容量代表了哈希表中桶的初始數(shù)量,即 Entry< K,V>[] table 數(shù)組的初始長(zhǎng)度,不過特別注意,table 數(shù)組的長(zhǎng)度雖然依賴 initialCapacity,但是每次都會(huì)通過 roundUpToPowerOf2(initialCapacity) 方法來保證為 2 的冪次。

          loadFactor 加載因子是哈希表在其容量自動(dòng)增加之前可以達(dá)到多滿的一種飽和度百分比,其衡量了一個(gè)散列表的空間的使用程度,負(fù)載因子越大表示散列表的裝填程度越高,反之愈小。散列當(dāng)前飽和度的計(jì)算為當(dāng)前 HashMap 中 Entry 的存儲(chǔ)個(gè)數(shù)除以當(dāng)前 table 數(shù)組桶長(zhǎng)度,因此當(dāng)哈希表中 Entry 的數(shù)量超過了 loadFactor 加載因子乘以當(dāng)前 table 數(shù)組桶長(zhǎng)度時(shí)就會(huì)觸發(fā)擴(kuò)容操作。對(duì)于使用鏈表法的散列表來說,查找一個(gè)元素的平均時(shí)間是O(1+a),因此如果負(fù)載因子越大則對(duì)空間的利用更充分,從而導(dǎo)致查找效率的降低,如果負(fù)載因子太小則散列表的數(shù)據(jù)將過于稀疏,從而對(duì)空間造成浪費(fèi)。系統(tǒng)默認(rèn)負(fù)載因子為 0.75,一般情況下無需修改。

          因此如果哈希桶數(shù)組很大則較差的 Hash 算法分部也會(huì)比較分散,如果哈希桶數(shù)組很小則即使好的 Hash 算法也會(huì)出現(xiàn)較多碰撞,所以就需要權(quán)衡好的 Hash 算法和擴(kuò)容機(jī)制,也就是上面兩個(gè)參數(shù)的作用。

          問:簡(jiǎn)單說說 JDK1.7 中 HashMap 什么情況下會(huì)發(fā)生擴(kuò)容?如何擴(kuò)容?

          答:HashMap 中默認(rèn)的負(fù)載因子為 0.75,默認(rèn)情況下第一次擴(kuò)容閥值是 12(16 * 0.75),故第一次存儲(chǔ)第 13 個(gè)鍵值對(duì)時(shí)就會(huì)觸發(fā)擴(kuò)容機(jī)制變?yōu)樵瓉頂?shù)組長(zhǎng)度的二倍,以后每次擴(kuò)容都類似計(jì)算機(jī)制;這也就是為什么 HashMap 在數(shù)組大小不變的情況下存放鍵值對(duì)越多查找的效率就會(huì)變低(因?yàn)?hash 碰撞會(huì)使數(shù)組變鏈表),而通過擴(kuò)容就可以一定程度的平衡查找效率(盡量散列數(shù)組化)的原因所在。

          下面給出 JDK 1.7 的具體擴(kuò)容流程:

          //JDK1.7擴(kuò)容最核心的方法,newTable為新容量數(shù)組大小
           
          void transfer(HashMapEntry[] newTable) {
           
             //新容量數(shù)組桶大小為舊的table的2倍
           
             int newCapacity = newTable.length;
           
             //遍歷舊的數(shù)組桶table
           
             for (HashMapEntry<K,V> e : table) {
           
                 //如果這個(gè)數(shù)組位置上有元素且存在哈希沖突的鏈表結(jié)構(gòu)則繼續(xù)遍歷鏈表
           
                 while(null != e) {
           
                     //取當(dāng)前數(shù)組索引位上單向鏈表的下一個(gè)元素
           
                     HashMapEntry<K,V> next = e.next;
           
                     //重新依據(jù)hash值計(jì)算元素在擴(kuò)容后數(shù)組中的索引位置
           
                     int i = indexFor(e.hash, newCapacity);
           
                     //將數(shù)組i的元素賦值給當(dāng)前鏈表元素的下一個(gè)節(jié)點(diǎn)
           
                     e.next = newTable[i];
           
                     //將鏈表元素放入數(shù)組位置
           
                     newTable[i] = e;
           
                     //將當(dāng)前數(shù)組索引位上單向鏈表的下一個(gè)元素賦值給e進(jìn)行新的一圈鏈表遍歷
           
                     e = next;
           
                 }
           
             }
           
          }

          可以看到,整個(gè)擴(kuò)容過程就是一個(gè)取出數(shù)組元素(實(shí)際數(shù)組索引位置上的每個(gè)元素是每個(gè)獨(dú)立單向鏈表的頭部,也就是發(fā)生 Hash 沖突后最后放入的沖突元素)然后遍歷以該元素為頭的單向鏈表元素,依據(jù)每個(gè)被遍歷元素的 hash 值計(jì)算其在新數(shù)組中的下標(biāo)然后進(jìn)行交換(即原來 hash 沖突的單向鏈表尾部變成了擴(kuò)容后單向鏈表的頭部)。下面給出圖解流程:

          問:簡(jiǎn)單說說 JDK 1.8 中 HashMap 是如何擴(kuò)容的?與 JDK 1.7 有什么區(qū)別?

          答:

          可以看見,1.7 中整個(gè)擴(kuò)容過程就是一個(gè)取出數(shù)組元素(實(shí)際數(shù)組索引位置上的每個(gè)元素是每個(gè)獨(dú)立單向鏈表的頭部,也就是發(fā)生 Hash 沖突后最后放入的沖突元素)然后遍歷以該元素為頭的單向鏈表元素,依據(jù)每個(gè)被遍歷元素的 hash 值計(jì)算其在新數(shù)組中的下標(biāo)然后進(jìn)行交換(即原來 hash 沖突的單向鏈表尾部變成了擴(kuò)容后單向鏈表的頭部)。

          而在 JDK 1.8 中 HashMap 的擴(kuò)容操作就顯得更加的騷氣了,由于擴(kuò)容數(shù)組的長(zhǎng)度是 2 倍關(guān)系,所以對(duì)于假設(shè)初始 tableSize =4 要擴(kuò)容到 8 來說就是 0100 到 1000 的變化(左移一位就是 2 倍),在擴(kuò)容中只用判斷原來的 hash 值與左移動(dòng)的一位按位與操作是 0 或 1 就行,0 的話索引就不變,1 的話索引變成原索引加上擴(kuò)容前數(shù)組。

          1.8 源碼核心實(shí)現(xiàn)如下:

          final Node<K,V>[] resize() {

            Node<K,V>[] oldTab = table;

            //記住擴(kuò)容前的數(shù)組長(zhǎng)度和最大容量

            int oldCap = (oldTab == null) ? 0 : oldTab.length;

            int oldThr = threshold;

            int newCap, newThr = 0;

            if (oldCap > 0) {

                //超過數(shù)組在java中最大容量就無能為力了,沖突就只能沖突

                if (oldCap >= MAXIMUM_CAPACITY) {

                    threshold = Integer.MAX_VALUE;

                    return oldTab;

                } //長(zhǎng)度和最大容量都擴(kuò)容為原來的二倍

                else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&

                         oldCap >= DEFAULT_INITIAL_CAPACITY)

                    newThr = oldThr << 1// double threshold

            }......

            ......

            //更新新的最大容量為擴(kuò)容計(jì)算后的最大容量

            threshold = newThr;

            //更新擴(kuò)容后的新數(shù)組長(zhǎng)度

            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];

            table = newTab;

            if (oldTab != null) {

                //遍歷老數(shù)組下標(biāo)索引

                for (int j = 0; j < oldCap; ++j) {

                    Node<K,V> e;

                    //如果老數(shù)組對(duì)應(yīng)索引上有元素則取出鏈表頭元素放在e中

                    if ((e = oldTab[j]) != null) {

                        oldTab[j] = null;

                        //如果老數(shù)組j下標(biāo)處只有一個(gè)元素則直接計(jì)算新數(shù)組中位置放置

                        if (e.next == null)

                            newTab[e.hash & (newCap - 1)] = e;

                        else if (e instanceof TreeNode) //如果是樹結(jié)構(gòu)進(jìn)行單獨(dú)處理

                            ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);

                        else { // preserve order

                            //能進(jìn)來說明數(shù)組索引j位置上存在哈希沖突的鏈表結(jié)構(gòu)

                            Node<K,V> loHead = null, loTail = null;

                            Node<K,V> hiHead = null, hiTail = null;

                            Node<K,V> next;

                            //循環(huán)處理數(shù)組索引j位置上哈希沖突的鏈表中每個(gè)元素

                            do {

                                next = e.next;

                                //判斷key的hash值與老數(shù)組長(zhǎng)度與操作后結(jié)果決定元素是放在原索引處還是新索引

                                if ((e.hash & oldCap) == 0) {

                                    //放在原索引處的建立新鏈表

                                    if (loTail == null)

                                        loHead = e;

                                    else

                                        loTail.next = e;

                                    loTail = e;

                                }

                                else {

                                    //放在新索引(原索引+oldCap)處的建立新鏈表

                                    if (hiTail == null)

                                        hiHead = e;

                                    else

                                        hiTail.next = e;

                                    hiTail = e;

                                }

                            } while ((e = next) != null);

                            if (loTail != null) {

                                //放入原索引處

                                loTail.next = null;

                                newTab[j] = loHead;

                            }

                            if (hiTail != null) {

                                //放入新索引處

                                hiTail.next = null;

                                newTab[j + oldCap] = hiHead;

                            }

                        }

                    }

                }

            }

            return newTab;

          }

          可以看見,這個(gè)設(shè)計(jì)非常贊,因?yàn)?hash 值本來就是隨機(jī)性的,所以 hash 按位與上 newTable 得到的 0(擴(kuò)容前的索引位置)和 1(擴(kuò)容前索引位置加上擴(kuò)容前數(shù)組長(zhǎng)度的數(shù)值索引處)就是隨機(jī)的,所以擴(kuò)容的過程就能把之前哈西沖突的元素再隨機(jī)的分布到不同的索引去,這算是 JDK1.8 的一個(gè)優(yōu)化點(diǎn)。

          此外,在 JDK1.7 中擴(kuò)容操作時(shí)哈西沖突的數(shù)組索引處的舊鏈表元素?cái)U(kuò)容到新數(shù)組時(shí)如果擴(kuò)容后索引位置在新數(shù)組的索引位置與原數(shù)組中索引位置相同,則鏈表元素會(huì)發(fā)生倒置(即如上面圖1,原來鏈表頭擴(kuò)容后變?yōu)槲舶停欢?JDK1.8 中不會(huì)出現(xiàn)鏈表倒置現(xiàn)象。

          其次,由于 JDK1.7 中發(fā)生哈西沖突時(shí)僅僅采用了鏈表結(jié)構(gòu)存儲(chǔ)沖突元素,所以擴(kuò)容時(shí)僅僅是重新計(jì)算其存儲(chǔ)位置而已,而 JDK1.8 中為了性能在同一索引處發(fā)生哈西沖突到一定程度時(shí)鏈表結(jié)構(gòu)會(huì)轉(zhuǎn)換為紅黑數(shù)結(jié)構(gòu)存儲(chǔ)沖突元素,故在擴(kuò)容時(shí)如果當(dāng)前索引中元素結(jié)構(gòu)是紅黑樹且元素個(gè)數(shù)小于鏈表還原閾值(哈西沖突程度常量)時(shí)就會(huì)把樹形結(jié)構(gòu)縮小或直接還原為鏈表結(jié)構(gòu)(其實(shí)現(xiàn)就是上面代碼片段中的 split() 方法)。


          如何快速降低一個(gè)員工的積極性?

          Java集合-List

          超經(jīng)典的 25 道 MyBatis 面試題!


          以上,便是今天的分享,希望大家喜歡,覺得內(nèi)容不錯(cuò)的,歡迎「分享」「」或者點(diǎn)擊「在看」支持,謝謝各位。

          瀏覽 24
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日本人妻の乱孕妇 | 北条麻妃在线a | 欧美黄色影片在线观看 | 国产免费操逼视频 | 国产精品18久久久 |