對於HashMap的容量的一些分析

来源:https://www.cnblogs.com/dollar/archive/2022/10/30/16610630.html
-Advertisement-
Play Games

1.邏輯 邏輯判斷:對於一件事情正確與否的判斷,python中用布爾類型真(True)、假(False)做區分; 根據判斷結果的不同去完成的不同操作,就是我們的業務邏輯; 對於條件是否滿足的判斷語句,就是條件語句; 一個邏輯語句是由條件語句+業務語句組成的。 2.if語句 判斷一個命題的真實性,如果 ...


在Java開發中,我們經常會像如下方式以下創建一個HashMap:
Map<String, String> map = new HashMap<String, String>();
但是上面的代碼中,我們並沒有給HashMap指定容量,那麼,這時候一個新創建的HashMap的預設容量是多少呢?為什麼呢?  

一、什麼是容量?

在Java中,保存數據有兩種比較簡單的數據結構:數組和鏈表。HashMap底層就是數組+鏈表(jdk1.8後底層是數組+鏈表+紅黑樹)。 在HashMap中,有兩個比較容易混淆的關鍵欄位:size和capacity ,這其中capacity就是Map的容量,而size我們稱之為Map中的元素個數。 簡單打個比方就更容易理解了:HashMap就是一個“桶”,那麼容量(capacity)就是這個桶當前最多可以裝多少元素--也就是數組的長度,而元素個數(size)表示這個桶已經裝了多少元素。 可以用以下代碼驗證一下:
Map<String, String> map = new HashMap<String, String>();
map.put("hello", "Java");
map.put("hell", "Java");
map.put("hel", "Java");
map.put("he", "Java");

/*
 * 通過反射獲取
 */
Class<?> mapType = map.getClass();
Method capacity = mapType.getDeclaredMethod("capacity");
capacity.setAccessible(true);
System.out.println("capacity : " + capacity.invoke(map));

Field size = mapType.getDeclaredField("size");
size.setAccessible(true);
System.out.println("size : " + size.get(map));
通過上面的代碼,我們發現了,當我們創建一個HashMap的時候,如果沒有指定其容量,那麼會得到一個預設容量為16的Map,那麼,這個容量是怎麼來的呢?又為什麼是這個數字呢? 實際上這個數字與hash有一定的關係,下麵我們看一下hash。  

二、什麼是hash?

我們知道,容量就是一個HashMap中”桶”的個數,那麼,當我們想要往一個HashMap中put一個元素的時候,需要通過一定的演算法計算出應該把他放到哪個桶中,這個過程就叫做hash。 hash的功能是根據key來確定這個key-value對在鏈表數組中的下標的。 那麼這個數組下標該怎麼確定呢?其實簡單,我們只要在hash方法中調用Object中的hashCode方法得到一個hash值,然後用這個值對HashMap的容量進行取模就可以得到數組下標了。 如果真的是這麼簡單的話,那HashMap的容量設置也就很簡單,但是考慮到效率等問題,HashMap的hash實現被設計的比較複雜,這也造成了HashMap的容量設置有一定限制。 下麵我們看一下hash的實現。  

三、hash的實現

hash的具體實現上,由兩個方法int hash(Object k)和int indexFor(int h, int length)(在jdk1.8後沒有這個方法了)來實現。
int hash(Object k):該方法主要是將Object轉換成一個整型數。 int indexFor(int h, int length):該方法主要是將hash()生成的整型數轉換成鏈表數組中的下標。
我們先來看下源碼:
static int indexFor(int h, int length) {
    return h & (length-1);
}
在JDK8中無indexFor方法,變為以下源碼中的i=(n-1)&hash hash方法中通過(h = k.hashCode()) ^ (h >>> 16)得到hash值 indexFor方法通過h & (length-1)得到下標。其中的兩個參數h表示元素的hash值,length表示HashMap的容量。 i=(n-1)&hash和上面的運算一樣。其中的兩個參數n表示HashMap的容量,hash表示元素的hash值。 那麼h & (length-1) 是什麼意思呢? 其實就是取模。Java之所以使用位運算(&)來代替取模運算(%),最主要的考慮就是效率
位運算(&)效率要比代替取模運算(%)高很多,主要原因是位運算直接對記憶體數據(二進位)進行操作,不需要轉成十進位,因此處理速度非常快。
那麼,為什麼可以使用位運算(&)來實現取模運算(%)呢?實現的原理如下:
a%b在當b為2^n時可簡化為a&(b-1)
證明:
1.當b為2^n時,b-1的二進位為011..11(0後面跟n個1).此時a&(b-1)即取a二進位右面n位
2.當b為2^n時,a/b的意義就是a右移n位。而右移的n位的值,就是a%b的值
理論不好理解,就找幾個例子用計算器試一下。
6 % 8 = 6 ,6 & 7 = 6
10 % 8 = 2 ,10 & 7 = 2
17 % 8 = 1 ,17 & 7 = 1
所以,h & (length-1)只要保證length是2^n的話,就可以實現取模運算了。 所以,因為位運算要比取模運算的效率更高,所以HashMap在計算元素要存放在數組中的下標的時候,使用位運算代替了取模運算。要實現位運算代替取模運算,要求HashMap的容量一定要是2^n 。 那麼,既然是2^n ,為啥一定要是16呢?為什麼不能是4、8或者32、64等等呢? 關於這個預設容量的選擇,JDK並沒有給出官方解釋。 這應該就是個經驗值,既然一定要設置一個預設的2^n 作為初始值,那麼就需要在效率和記憶體使用上做一個權衡。 4、8太小了就有可能頻繁發生擴容,擴容就需要重建哈希表,影響效率。32、64等等太大了又浪費空間,不划算。 所以,16就作為一個經驗值被採用了。  
在JDK 8中,關於預設容量的定義如上源碼 ,其故意把16寫成1<<4,就是提醒開發者,這個地方要是2的冪。註釋中的 aka 16 也是jdk1.8中新增的,Also Known As 16 -- 又名16
既然我們需要把容量設置為2^n,那麼HashMap是如何保證其容量一定可以是2^n的呢? 要做到這一類型容量,HashMap在指定容量初始化時以及自動擴容時都做了處理。  

四、指定容量初始化

以下是《Java開發手冊》中的一段話: 可以看到指定容量初始化是被推薦的。 當我們通過HashMap(int initialCapacity)這個構造方法設置初始容量的時候,HashMap並不一定會直接採用我們傳入的initialCapacity,而是經過計算,得到一個新initialCapacity(例如3變為4,9變為16)。 以下為初始化的源碼: 可以看到最底層就是用tableSizeFor方法得到最終的初始化容量。 可以把代碼分為Part 1:
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
和Part 2:
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
Part 2比較簡單,就是做一下判斷,然後把Part 1得到的數值+1。 Part 1怎麼理解呢?其實是對一個二進位數依次無符號右移,然後與原值取或。 拿一個二進位數,套一遍上面的公式就發現其目的:
1100 1100 1100 >>>1 = 0110 0110 0110
1100 1100 1100 | 0110 0110 0110 = 1110 1110 1110
1110 1110 1110 >>>2 = 0011 1011 1011
1110 1110 1110 | 0011 1011 1011 = 1111 1111 1111
1111 1111 1111 >>>4 = 1111 1111 1111
1111 1111 1111 | 1111 1111 1111 = 1111 1111 1111
···以此類推
通過幾次無符號右移和按位或運算,我們把1100 1100 1100轉換成了1111 1111 1111 ,再把1111 1111 1111加1,就得到了1 0000 0000 0000,次數就是大於1100 1100 1100的第一個2的冪。 可以用程式試驗一下:
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
System.out.println((n < 0) ? 1 : (n >= 1 << 30) ? 1 << 30 : n + 1); 
 

五、自動擴容

指定容量初始化後,往集合中put元素時,元素個數超過閾值後怎麼辦呢?這時候就需要擴容了。 HashMap有擴容機制,就是當達到擴容條件時會進行擴容。擴容條件就是當HashMap中的元素個數(size)超過閾值(threshold)時就會自動擴容,通過resize方法進行擴容。 threshold = loadFactor * capacity。 loadFactor是負載因數,預設值為0.75f,設置成0.75有一個好處,那就是0.75正好是3/4,而capacity又是2的冪。所以,兩個數的乘積都是整數。 對於一個預設的HashMap來說,預設情況下,當其size大於12(16*0.75)時就會觸發擴容。 下麵是HashMap中的resize方法中的一段源碼:  
newCap = oldCap << 1;
newThr = oldThr << 1;
從上面兩行代碼可以看出,擴容後的capacity和threshold變為原來的兩倍。 可見,當HashMap中size>threshold時就會自動擴容,擴容成原容量的2倍,即從16擴容到32、64、128 …閾值也從12到24,48,96... 所以,通過保證初始化容量均為2的冪,並且擴容時也是擴容到之前容量的2倍,所以,保證了HashMap的容量永遠都是2的冪,從而保證了位運算代替取模運算,從而提升了效率。   可以看到初始化之後容量和閾值是一樣的, 當放入第一個元素後,重新計算閾值,新的閾值=容量X負載因數。 可以用程式實驗一下:
HashMap m = new HashMap(15);
                
Class<?> mapType = m.getClass();
Field threshold = mapType.getDeclaredField("threshold");
threshold.setAccessible(true);
Method capacity = mapType.getDeclaredMethod("capacity");
capacity.setAccessible(true);
                
System.out.println("容量:" + capacity.invoke(m) + "    閾值:" + threshold.get(m) + "    元素數量:" + m.size());
                
for (int i = 0; i < 25; i++) {
    m.put(i, i);
    System.out.println("容量:" + capacity.invoke(m) + "    閾值:" + threshold.get(m) + "    元素數量:" + m.size());
}

六、總結

HashMap作為一種數據結構,元素在put的過程中需要進行hash運算,目的是計算出該元素存放在hashMap中的具體位置。 hash運算的過程其實就是先對key用hash()方法得到一個hash值,再用hash值對Map的容量進行取模得到下標,而JDK的工程師為了提升取模的效率,使用位運算代替了取模運算,這就要求Map的容量一定得是2的冪。 為了保證任何情況下Map的容量都是2的冪,HashMap在兩個地方都做了限制。 首先是,如果用戶制定了初始容量,那麼HashMap會計算出比該數大的第一個2的冪作為初始容量。 另外,在擴容的時候,也是進行成倍的擴容,即16變成32,32變成64。
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 我們都知道,我們寫的Java程式需要先經過編譯,生成了.class文件(位元組碼文件)。然而,電腦並不能直接解釋.class文件裡面的內容,這時候就需要一個能載入、解釋.class文件並且能按.class文件里的內容進行處理的一個東西--JVM。 JVM,就是Java虛擬機。它是一種規範,有針對不同 ...
  • Count15 module top_module ( input clk, input reset, // Synchronous active-high reset output [3:0] q); always@(posedge clk) begin if(reset) q <= 4'd0; ...
  • 2022-10-30 連接資料庫的搭建環境 一、搭建環境 ①導入jar包(資料庫驅動包、資料庫連接池、DBUtils) jar包有:commons-dbutils-1.4.jar、 druid-1.0.9.jar 、mysql-connector-java-8.0.19.jar。 方式:在創建的“W ...
  • 本篇文章,將從 0 淺入,從什麼是哈希表講起,然後再說 Java 是怎樣實現哈希表的。整個梳理過程,將通過源碼這個第一手的資料進行梳理分析,吸收知識、解決疑問,一步一步進行梳理,如果你是對 HashMap 懵懵懂懂的同學,那麼歡迎跟著我的節奏一起來梳理!全文1萬2000多字,歡迎慢慢食用! ...
  • JavaScript01 官方文檔 http://www.w3school.com.cn/js/index.asp 基本說明: JavaScript能改變html內容,能改變html屬性,能改變html樣式(css),能完成頁面的數據驗證。 例子 <!DOCTYPE html> <html lang ...
  • SpringBoot 階段測試 1 1、使用JDK8新語法完成下列集合練習: 1.1 List中有1,2,3,4,5,6,7,8,9幾個元素要求; (1) 將奇、偶數分別匯聚成一個List //初始化集合 List<Integer> numList = new ArrayList<>(); //批量 ...
  • 1、項目介紹 2、微信公眾平臺 和 微信開放文檔 2.1 微信公眾平臺 2.1.1 網址鏈接 https://mp.weixin.qq.com/debug/cgi-bin/sandboxinfo?action=showinfo&t=sandbox/index 2.1.2 測試號信息 2.1.3 微信 ...
  • C Primer Plus 摘錄 第 10 章 數組和指針 10.1 數組 數組由數據類型相同的一系列元素組成。 通過聲明數組告訴編譯器數組中內含多少元素和這些元素的類型。 編譯器根據這些信息正確地創建數組。 float candy[365]; char code[12]; int states[5 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...