1. 程式人生 > >面試官:"準備用HashMap存1w條資料,構造時傳10000還會觸發擴容嗎?"

面試官:"準備用HashMap存1w條資料,構造時傳10000還會觸發擴容嗎?"

// 預計存入 1w 條資料,初始化賦值 10000,避免 resize。
HashMap<String,String> map = new HashMap<>(10000)
// for (int i = 0; i < 10000; i++)

Java 集合的擴容

HashMap 算是我們最常用的集合之一,雖然對於 Android 開發者,Google 官方推薦了更省記憶體的 SparseArray 和 ArrayMap,但是 HashMap 依然是最常用的。

我們通過 HashMap 來儲存 Key-Value 這種鍵值對形式的資料,其內部通過雜湊表,讓存取效率最好時可以達到 O(1),而又因為可能存在的 Hash 衝突,引入了連結串列和紅黑樹的結構,讓效率最差也差不過 O(logn)。

整體來說,HashMap 作為一款工業級的雜湊表結構,效率還是有保障的。

程式語言提供的集合類,雖然底層還是基於陣列、連結串列這種最基本的資料結構,但是和我們直接使用陣列不同,集合在容量不足時,會觸發動態擴容來保證有足夠的空間儲存資料。

動態擴容,涉及到資料的拷貝,是一種「較重」的操作。那如果能夠提前確定集合將要儲存的資料量範圍,就可以通過構造方法,指定集合的初始容量,來保證接下來的操作中,不至於觸發動態擴容。

這就引入了本文開篇的問題,如果使用 HashMap,當初始化是建構函式指定 1w 時,後續我們立即存入 1w 條資料,是否符合與其不會觸發擴容呢?

在分析這個問題前,那我們先來看看,HashMap 初始化時,指定初始容量值都做了什麼?

PS:本文所涉及程式碼,均以 JDK 1.8 中 HashMap 的原始碼舉例。

HashMap 的初始化

在 HashMap 中,提供了一個指定初始容量的構造方法 HashMap(int initialCapacity),這個方法最終會呼叫到 HashMap 另一個構造方法,其中的引數 loadFactor 就是預設值 0.75f。

public HashMap(int initialCapacity, float loadFactor) {
  if (initialCapacity < 0)
    throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
  if (initialCapacity > MAXIMUM_CAPACITY)
    initialCapacity = MAXIMUM_CAPACITY;
  if (loadFactor <= 0 || Float.isNaN(loadFactor))
    throw new IllegalArgumentException("Illegal load factor: " + loadFactor);

  this.loadFactor = loadFactor;
  this.threshold = tableSizeFor(initialCapacity);
}

其中的成員變數 threshold 就是用來儲存,觸發 HashMap 擴容的閾值,也就是說,當 HashMap 儲存的資料量達到 threshold 時,就會觸發擴容。

從構造方法的邏輯可以看出,HashMap 並不是直接使用外部傳遞進來的 initialCapacity,而是經過了 tableSizeFor() 方法的處理,再賦值到 threshole 上。

static final int tableSizeFor(int cap) {
  int n = cap - 1;
  n |= n >>> 1;
  n |= n >>> 2;
  n |= n >>> 4;
  n |= n >>> 8;
  n |= n >>> 16;
  return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

tableSizeFor() 方法中,通過逐步位運算,就可以讓返回值,保持在 2 的 N 次冪。以方便在擴容的時候,快速計算資料在擴容後的新表中的位置。

那麼當我們從外部傳遞進來 1w 時,實際上經過 tableSizeFor() 方法處理之後,就會變成 2 的 14 次冪 16384,再算上負載因子 0.75f,實際在不觸發擴容的前提下,可儲存的資料容量是 12288(16384 * 0.75f)。

這種場景下,用來存放 1w 條資料,綽綽有餘了,並不會觸發我們猜想的擴容。

HashMap 的 table 初始化

當我們把初始容量,調整到 1000 時,情況又不一樣了,具體情況具體分析。

再回到 HashMap 的構造方法,threshold 為擴容的閾值,在構造方法中由 tableSizeFor() 方法調整後直接賦值,所以在構造 HashMap 時,如果傳遞 1000,threshold 調整後的值確實是 1024,但 HashMap 並不直接使用它。

仔細想想就會知道,初始化時決定了 threshold 值,但其裝載因子(loadFactor)並沒有參與運算,那在後面具體邏輯的時候,HashMap 是如何處理的呢?

在 HashMap 中,所有的資料,都是通過成員變數 table 陣列來儲存的,在 JDK 1.7 和 1.8 中雖然 table 的型別有所不同,但是陣列這種基本結構並沒有變化。那麼 table、threshold、loadFactor 三者之間的關係,就是:

table.size == threshold * loadFactor

那這個 table 是在什麼時候初始化的呢?這就要說會到我們一直在迴避的問題,HashMap 的擴容。

在 HashMap 中,動態擴容的邏輯在 resize() 方法中。這個方法不僅僅承擔了 table 的擴容,它還承擔了 table 的初始化。

當我們首次呼叫 HashMap 的 put() 方法存資料時,如果發現 table 為 null,則會呼叫 resize() 去初始化 table,具體邏輯在 putVal() 方法中。

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
    n = (tab = resize()).length; // 呼叫 resize()
    // ...
}

resize() 方法中,調整了最終 threshold 值,以及完成了 table 的初始化。

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    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; 
    }
    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; // ③
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab; // ④
    // ....
}

注意看程式碼中的註釋標記。

因為 resize() 還糅合了動態擴容的邏輯,所以我將初始化 table 的邏輯用註釋標記出來了。其中 xxxCap 和 xxxThr 分別對應了 table 的容量和動態擴容的閾值,所以存在舊和新兩組資料。

當我們指定了初始容量,且 table 未被初始化時,oldThr 就不為 0,則會走到程式碼 ① 的邏輯。在其中將 newCap 賦值為 oldThr,也就是新建立的 table 會是我們構造的 HashMap 時指定的容量值。

之後會進入程式碼 ② 的邏輯,其中就通過裝載因子(loadFactor)調整了新的閾值(newThr),當然這裡也做了一些限制需要讓 newThr 在一個合法的範圍內。

在程式碼 ③ 中,將使用 loadFactor 調整後的閾值,重新儲存到 threshold 中。並通過 newCap 建立新的陣列,將其指定到 table 上,完成 table 的初始化(程式碼 ④)。

到這裡也就清楚了,雖然我們在初始化時,傳遞進來的 initialCapacity 雖然被賦值給 threshold,但是它實際是 table 的尺寸,並且最終會通過 loadFactor 重新調整 threshold

那麼回到之前的問題就有答案了,雖然 HashMap 初始容量指定為 1000,但是它只是表示 table 陣列為 1000,擴容的重要依據擴容閾值會在 resize() 中調整為 768(1024 * 0.75)。

它是不足以承載 1000 條資料的,最終在存夠 1k 條資料之前,還會觸發一次動態擴容。

通常在初始化 HashMap 時,初始容量都是根據業務來的,而不會是一個固定值,為此我們需要有一個特殊處理的方式,就是將預期的初始容量,再除以 HashMap 的裝載因子,預設時就是除以 0.75。

例如想要用 HashMap 存放 1k 條資料,應該設定 1000 / 0.75,實際傳遞進去的值是 1333,然後會被 tableSizeFor() 方法調整到 2048,足夠儲存資料而不會觸發擴容。

當想用 HashMap 存放 1w 條資料時,依然設定 10000 / 0.75,實際傳遞進去的值是 13333,會被調整到 16384,和我們直接傳遞 10000 效果是一樣的。

小結時刻

到這裡,就瞭解清楚了 HashMap 的初始容量,應該如何科學的計算,本質上你傳遞進去的值可能並無法直接儲存這麼多資料,會有一個動態調整的過程。其中就需要將我們預期的值進行放大,比較科學的就是依據裝載因子進行放大。

最後我們再總結一下:

  1. HashMap 構造方法傳遞的 initialCapacity,雖然在處理後被存入了 loadFactor 中,但它實際表示 table 的容量。
  2. 構造方法傳遞的 initialCapacity,最終會被 tableSizeFor() 方法動態調整為 2 的 N 次冪,以方便在擴容的時候,計算資料在 newTable 中的位置。
  3. 如果設定了 table 的初始容量,會在初始化 table 時,將擴容閾值 threshold 重新調整為 table.size * loadFactor。
  4. HashMap 是否擴容,由 threshold 決定,而 threshold 又由初始容量和 loadFactor 決定。
  5. 如果我們預先知道 HashMap 資料量範圍,可以預設 HashMap 的容量值來提升效率,但是需要注意要考慮裝載因子的影響,才能保證不會觸發預期之外的動態擴容。

HashMap 作為 Java 最常用的集合之一,市面上優秀的文章很多,但是很少有人從初始容量的角度來分析其中的邏輯,而初始容量又是集合中比較實際的優化點。其實不少人也搞不清楚,在設定 HashMap 初始容量時,是否應該考慮裝載因子,才有了此文。

如果本文對你有所幫助,留言、轉發、點好看是最大的支援,謝謝!


公眾號後臺回覆成長『成長』,將會得到我準備的學習資料,也能回覆『加群』,一起學習進步。

相關推薦

面試&quot;備用HashMap1w資料構造10000觸發擴容?&quot;

// 預計存入 1w 條資料,初始化賦值 10000,避免 resize。 HashMap<String,String> map = new HashMap<>(10000) // for (int i = 0; i < 10000; i++) Java 集合的擴容

面試CPU百分百!給你一分鐘怎麼排查?有幾種方法?

Part0 遇到了故障怎麼辦? 在生產上,我們會遇到各種各樣的故障,遇到了故障怎麼辦? 不要慌,只有冷靜才是解決故障的利器。 下面以一個例子為例,在生產中碰到了CPU 100%的問題怎麼辦? 在生產中真的碰到了CPU 100%的問題,再來看這篇文章已經遲了,還是先來模擬演練下吧。 怎麼模擬演練? (1)查詢資

面試“看你簡歷上寫熟悉 Handler 機制那聊聊 IdleHandler 吧?”

一. 序 Handler 機制算是 Android 基本功,面試常客。但現在面試,多數已經不會直接讓你講講 Handler 的機制,Looper 是如何迴圈的,MessageQueue 是如何管理 Message 等,而是基於場景去提問,看看你對 Handler 機制的掌握是否紮實。 本文就來聊聊 H

面試你剛說你喜歡研究新技術那麼請說說你對 Blazor 的瞭解

閱讀本文大概需要 1.5 分鐘。 最近在幾個微信 .NET 交流群裡大家討論比較頻繁的話題就是這幾天自己的面試經歷。 面試官:“你剛說你喜歡研究新技術,那麼你對 Blazor 瞭解多少?”。 作為一位專注於 .NET 開發的軟體工程師,你好意思說你對 Blazor 一點也不解嗎?.NET 新技術也就是那

面試如果讓你寫個分散式配置中心就問你慌不慌

## 前言 一位讀者朋友跟我反饋,能不能寫一篇比較全的配置中心的文章。自己最近在面試過程中有被面試官問:**如何設計一個配置中心?** 這個話題,由於自己在工作中也沒實際使用過配置中心,所以對於如何去設計是完全沒有概念的。 今天就給大家寫一篇去配置中心需要考慮的點,我也不是什麼配置中心開源專案的參與者,所

阿里面試HashMap資料結構之道

    問題1:HashMap的資料結構是什麼樣的? 同學1:嗯...陣列+連結串列 同學2:陣列

阿里面試HashMap 熟悉吧?好的那就來聊聊 Redis 字典吧!

最近,小黑哥的一個朋友出去面試,回來跟小黑哥抱怨,面試官不按套路出牌,直接打亂了他的節奏。 事情是這樣的,前面面試問了幾個 Java 的相關問題,我朋友回答還不錯,接下來面試官就問了一句:看來 Java 基礎還不錯,Java HashMap 你熟悉吧? 我朋友回答。工作經常用,有看過原始碼。 我朋友本來

面試分散式事務講下 程式設計師不清楚 然後結果就涼涼了

java、後端開發、程式設計師、分散式事務 分散式事務應該是面試官最喜歡問的題目之一 我對分散式事務的基本思路整理總結了一下,其實還有很多細節沒研究。 基礎知識準備 資料庫事務、分散式、微服務、分庫分表 資料庫事務的特性:原子性(Atomicity )、一致性( Cons

面試快排

快排可以說是一道必知的常見面試題,同時也有多種實現方式。在這篇文章中,我使用的是隨機三路快排。 之所以使用隨機快速排序而不是普通的快排。是因為前者可以使得數列有序的概率降低,從而使隨機快速排序平均速度是比快速排序要快的。具體的兩者的效能差別可以看下這篇文章: blog.csdn.net/haelang/a

面試說說快速失敗和安全失敗是什麼

什麼是快速失敗(fail-fast)和安全失敗(fail-safe)?它們又和什麼內容有關係。以上兩點就是這篇文章的內容,廢話不多話,正文請慢用。 我們都接觸 HashMap、ArrayList 這些集合類,這些在 java.util 包的集合類就都是快速失敗的;而 java.util.concurrent

面試被當成菜鳥程式設計師當場摘帽子面試明天來上班!

很多求職者,都有過面試的經歷,這個過程很讓人煎熬,因為面試前需要做很多準備,比如修改簡歷、準備面試內容,甚至還要思考面試時要怎麼穿著才得體。 雖然說光看外表並不能客觀的反映一個人的真實能力,但是面試官也會通過求職者的外形和裝扮來判斷他們的經驗和閱歷。 學習web前端找工作這裡推

程式設計師面試稱自己“理想就是不上班”面試這樣能收

在求職過程中,作為求職者在回答面試官提出的所有問題時都需要“實話實說”嗎?就有一名領導在面試一個應屆程式設計師時,本來覺得其能力很好準備收了。最後快結束的時候隨口問了一句現在的工作是不是他理想的工作?沒想到這名程式設計師很直接的回答稱“我上班就是為了掙錢,不想談理想,我的理想就是不上班!”求職者這樣

面試"我為什麼要聘用你"

關於面試,面試官也是人,人的想法可能千奇百怪,雖然其中有一定的規律可以循,但是不乏意料之外的問題。老師的工作是為學生開啟一扇門,讓學生自己走進去,不能使勁把分們拉進來,因為走進來必須是學生自己的事情。講得再多,沒有體悟也是按圖索驥。面試的套路可以說上“兵無常勢,水無常形”能因

大專程式設計師面試簡歷當場被撕面試大專生我們不收

每個人都有過求職的經歷,這個過程也是很痛苦的,因為求職不可能會讓你一帆風順的,經常會使你碰壁。即便是被拒絕了之後,想想自己的哪些方面的不足,在下一次面試的時候做好準備,也沒什麼抱怨的。但是總有一些面試官的做法卻讓人感到憤怒。 就有一名程式設計師發帖講述了自己最近的一次面試被面試官撕掉簡歷的

怎麼回答面試你對Spring的理解

spring呢,是pivotal公司維護的一系列開源工具的總稱,最為人所知的是spring mvc,事實上,他們都是基於spring framework,並且再其上繼續增強,為某一方面服務的java元件。最近spring framework 剛升級到5,非常不錯。比較常見的有

面試你為什麼要離開之前的公司?

離職、跳槽是一件很正常的事。但是難免在面試時會被問到:“你為什麼要離開之前的公司?”首先,你要知道面試官這麼問的目的是什麼?面試官問這個問題,是想知道你離開之前公司的原因是否合理,是想知道你是否對公司忠誠、有熱情、感興趣,想考察你動機是否單純、是否只是把公司當成一個“避難所”

面試給我說一下你項目中的單點登錄是如何實現的?

分享圖片 .get 監聽 rec 返回 例子 比對 .exe 功能 一、單系統登錄機制 1、http無狀態協議 web應用采用browser/server架構,http作為通信協議。http是無狀態協議,瀏覽器的每一次請求,服務器會獨立處理,不與之前或之後的請求產生關聯,這

面試你分析過SpringMVC的源碼

技術分享 rop 調用 直接 setview gate rem code 上傳 1. MVC使用 在研究源碼之前,先來回顧以下springmvc 是如何配置的,這將能使我們更容易理解源碼。 1.1 web.xml <servlet> <servle

面試說說一查詢sql的執行流程和底層原理?

序章 自我介紹 我是一條sql,就是一條長長的字串,不要問我長什麼樣,因為我比較傲嬌。   額~~不是我不

【BAT面試題系列】面試你了解樂觀鎖和悲觀鎖

次數 catch val util overflow info 基本概念 因此 問題 前言 樂觀鎖和悲觀鎖問題,是出現頻率比較高的面試題。本文將由淺入深,逐步介紹它們的基本概念、實現方式(含實例)、適用場景,以及可能遇到的面試官追問,希望能夠幫助你打動面試官。 目錄