hashmap remove 沒釋放內存_java從零開始手寫 redis(13)HashMap 源碼原理詳解

為什么學習 HashMap 源碼?

作為一名 java 開發,基本上最常用的數據結構就是 HashMap 和 List,jdk 的 HashMap 設計還是非常值得深入學習的。

無論是在面試還是工作中,知道原理都對會我們有很大的幫助。

本篇的內容較長,建議先收藏,再細細品味。

不同于網上簡單的源碼分析,更多的是實現背后的設計思想。

涉及的內容比較廣泛,從統計學中的泊松分布,到計算機基礎的位運算,經典的紅黑樹、鏈表、數組等數據結構,也談到了 Hash 函數的相關介紹,文末也引入了美團對于 HashMap 的源碼分析,所以整體深度和廣度都比較大。

思維導圖如下:

d39930111d8e5615ab33c4cccb07dc1c.png
思維導圖

本文是兩年前整理的,文中不免有疏漏過時的地方,歡迎大家提出寶貴的意見。

之所以這里拿出來,有以下幾個目的:

(1)讓讀者理解 HashMap 的設計思想,知道 rehash 的過程。下一節我們將自己實現一個 HashMap

(2)為什么要自己實現 HashMap?

最近在手寫 redis 框架,都說 redis 是一個特性更加強大的 Map,自然 HashMap 就是入門基礎。Redis 高性能中一個過人之處的設計就是漸進式 rehash,和大家一起實現一個漸進式 rehash 的 map,更能體會和理解作者設計的巧妙。

想把常見的數據結構獨立為一個開源工具,便于后期使用。比如這次手寫 redis,循環鏈表,LRU map 等都是從零開始寫的,不利于復用,還容易有 BUG。

好了,下面就讓我們一起開始 HashMap 的源碼之旅吧~

HashMap 源碼

HashMap 是平時使用到非常多的一個集合類,感覺有必要深入學習一下。

首先嘗試自己閱讀一遍源碼。

java 版本

$?java?-version
java?version?"1.8.0_91"
Java(TM)?SE?Runtime?Environment?(build?1.8.0_91-b14)
Java?HotSpot(TM)?64-Bit?Server?VM?(build?25.91-b14,?mixed?mode)

數據結構

從結構實現來講,HashMap是數組+鏈表+紅黑樹(JDK1.8增加了紅黑樹部分)實現的。

對于當前類的官方說明

基于哈希表實現的映射接口。這個實現提供了所有可選的映射操作,并允許空值和空鍵。(HashMap類大致相當于Hashtable,但它是非同步的,并且允許為空。)

這個類不保證映射的順序;特別地,它不能保證順序會隨時間保持不變。

這個實現為基本操作(get和put)提供了恒定時間的性能,假設哈希函數將元素適當地分散在各個桶中。對集合視圖的迭代需要與HashMap實例的“容量”(桶數)及其大小(鍵-值映射數)成比例的時間。因此,如果迭代性能很重要,那么不要將初始容量設置得太高(或者負載系數太低),這是非常重要的。

HashMap實例有兩個影響其性能的參數: 初始容量和負載因子

容量是哈希表中的桶數,初始容量只是創建哈希表時的容量。負載因子是在哈希表的容量自動增加之前,哈希表被允許達到的最大容量的度量。當哈希表中的條目數量超過負載因子和當前容量的乘積時,哈希表就會被重新哈希(也就是說,重新構建內部數據結構),這樣哈希表的桶數大約是原來的兩倍。

一般來說,默認的負載因子(0.75)在時間和空間成本之間提供了很好的權衡。

較高的值減少了空間開銷,但增加了查找成本(反映在HashMap類的大多數操作中,包括get和put)。在設置映射的初始容量時,應該考慮映射中的期望條目數及其負載因子,以最小化重哈希操作的數量。如果初始容量大于條目的最大數量除以負載因子,就不會發生重哈希操作。

如果要將許多映射存儲在HashMap實例中,那么使用足夠大的容量創建映射將使映射存儲的效率更高,而不是讓它根據需要執行自動重哈希以增長表。

注意,使用具有相同hashCode()的多個鍵確實可以降低任何散列表的性能。為了改善影響,當鍵具有可比性時,這個類可以使用鍵之間的比較順序來幫助斷開連接。

注意,這個實現不是同步的。如果多個線程并發地訪問散列映射,并且至少有一個線程在結構上修改了映射,那么它必須在外部同步。(結構修改是添加或刪除一個或多個映射的任何操作;僅更改與實例已經包含的鍵關聯的值并不是結構修改。這通常是通過對自然封裝映射的對象進行同步來完成的。

如果不存在這樣的對象,則應該使用集合“包裝” Collections.synchronizedMap 方法。這最好在創建時完成,以防止意外的對映射的非同步訪問:

Map?m?=?Collections.synchronizedMap(new?HashMap(...));

這個類的所有“集合視圖方法”返回的迭代器都是快速失敗的:如果在創建迭代器之后的任何時候對映射進行結構上的修改,除了通過迭代器自己的remove方法,迭代器將拋出ConcurrentModificationException。因此,在并發修改的情況下,迭代器會快速而干凈地失敗,而不是在未來的不確定時間內冒著任意的、不確定的行為的風險。

注意,迭代器的快速故障行為不能得到保證,因為一般來說,在存在非同步并發修改的情況下,不可能做出任何硬性保證。快速失敗迭代器以最佳的方式拋出ConcurrentModificationException。因此,編寫依賴于此異常的程序來保證其正確性是錯誤的:迭代器的快速故障行為應該僅用于檢測錯誤。

其他基礎信息

  1. 這個類是Java集合框架的成員。

  2. @since 1.2

  3. java.util 包下

源碼初探

接口

public?class?HashMap<K,V>?extends?AbstractMap<K,V>implements?Map<K,V>,?Cloneable,?Serializable?{}

當前類實現了三個接口,我們主要關心 Map 接口即可。

繼承了一個抽象類 AbstractMap,這個暫時放在本節后面學習。

常量定義

默認初始化容量

/**
?*?The?default?initial?capacity?-?MUST?be?a?power?of?two.
?*/
static?final?int?DEFAULT_INITIAL_CAPACITY?=?1?<4;?//?aka?16
  • 為什么不直接使用 16?

看了下 statckoverflow,感覺比較靠譜的解釋是:

  1. 為了避免使用魔法數字,使得常量定義本身就具有自我解釋的含義。

  2. 強調這個數必須是 2 的冪。

  • 為什么要是 2 的冪?

它是這樣設計的,因為它允許使用快速位和操作將每個鍵的哈希代碼包裝到表的容量范圍內,正如您在訪問表的方法中看到的:

final?Node?getNode(int?hash,?Object?key)?{
????Node[]?tab;?Node?first,?e;?int?n;?K?k;if?((tab?=?table)?!=?null?&&?(n?=?tab.length)?>?0?&&
????????(first?=?tab[(n?-?1)?&?hash])?!=?null)?{?///?
????????...

最大容量

隱式指定較高值時使用的最大容量。

由任何帶有參數的構造函數。

必須是2的冪且小于 1<<30。

/**
*?The?maximum?capacity,?used?if?a?higher?value?is?implicitly?specified
*?by?either?of?the?constructors?with?arguments.
*?MUST?be?a?power?of?two?<=?1<<30.
*/
static?final?int?MAXIMUM_CAPACITY?=?1?<30;
  • 為什么是 1 << 30?

當然了 interger 的最大容量為 2^31-1

除此之外,2**31是20億,每個哈希條目需要一個對象作為條目本身,一個對象作為鍵,一個對象作為值。

在為應用程序中的其他內容分配空間之前,最小對象大小通常為24字節左右,因此這將是1440億字節。

可以肯定地說,最大容量限制只是理論上的。

感覺實際內存也沒這么大!

負載因子

當負載因子較大時,去給table數組擴容的可能性就會少,所以相對占用內存較少(空間上較少),但是每條entry鏈上的元素會相對較多,查詢的時間也會增長(時間上較多)。

反之就是,負載因子較少的時候,給table數組擴容的可能性就高,那么內存空間占用就多,但是entry鏈上的元素就會相對較少,查出的時間也會減少。

所以才有了負載因子是時間和空間上的一種折中的說法。

所以設置負載因子的時候要考慮自己追求的是時間還是空間上的少。

/**
?*?The?load?factor?used?when?none?specified?in?constructor.
?*/
static?final?float?DEFAULT_LOAD_FACTOR?=?0.75f;
  • 為什么是 0.75,不是 0.8 或者 0.6

其實 hashmap 源碼中有解釋。

Because?TreeNodes?are?about?twice?the?size?of?regular?nodes,?we
use?them?only?when?bins?contain?enough?nodes?to?warrant?use
(see?TREEIFY_THRESHOLD).?And?when?they?become?too?small?(due?to
removal?or?resizing)?they?are?converted?back?to?plain?bins.??In
usages?with?well-distributed?user?hashCodes,?tree?bins?are
rarely?used.??Ideally,?under?random?hashCodes,?the?frequency?of
nodes?in?bins?follows?a?Poisson?distribution
(http://en.wikipedia.org/wiki/Poisson_distribution)?with?a
parameter?of?about?0.5?on?average?for?the?default?resizing
threshold?of?0.75,?although?with?a?large?variance?because?of
resizing?granularity.?Ignoring?variance,?the?expected
occurrences?of?list?size?k?are?(exp(-0.5)?*?pow(0.5,?k)?/
factorial(k)).?The?first?values?are:

0:????0.60653066
1:????0.30326533
2:????0.07581633
3:????0.01263606
4:????0.00157952
5:????0.00015795
6:????0.00001316
7:????0.00000094
8:????0.00000006
more:?less?than?1?in?ten?million

簡單翻譯一下就是在理想情況下,使用隨機哈希碼,節點出現的頻率在hash桶中遵循泊松分布,同時給出了桶中元素個數和概率的對照表。

從上面的表中可以看到當桶中元素到達8個的時候,概率已經變得非常小,也就是說用0.75作為加載因子,每個碰撞位置的鏈表長度超過8個是幾乎不可能的。

Poisson distribution —— 泊松分布

閾值

/**
?*?The?bin?count?threshold?for?using?a?tree?rather?than?list?for?a
?*?bin.??Bins?are?converted?to?trees?when?adding?an?element?to?a
?*?bin?with?at?least?this?many?nodes.?The?value?must?be?greater
?*?than?2?and?should?be?at?least?8?to?mesh?with?assumptions?in
?*?tree?removal?about?conversion?back?to?plain?bins?upon
?*?shrinkage.
?*/
static?final?int?TREEIFY_THRESHOLD?=?8;

/**
?*?The?bin?count?threshold?for?untreeifying?a?(split)?bin?during?a
?*?resize?operation.?Should?be?less?than?TREEIFY_THRESHOLD,?and?at
?*?most?6?to?mesh?with?shrinkage?detection?under?removal.
?*/
static?final?int?UNTREEIFY_THRESHOLD?=?6;

/**
?*?The?smallest?table?capacity?for?which?bins?may?be?treeified.
?*?(Otherwise?the?table?is?resized?if?too?many?nodes?in?a?bin.)
?*?Should?be?at?least?4?*?TREEIFY_THRESHOLD?to?avoid?conflicts
?*?between?resizing?and?treeification?thresholds.
?*/
static?final?int?MIN_TREEIFY_CAPACITY?=?64;

TREEIFY_THRESHOLD

使用紅黑樹而不是列表的bin count閾值。

當向具有至少這么多節點的bin中添加元素時,bin被轉換為樹。這個值必須大于2,并且應該至少為8,以便與樹刪除中關于收縮后轉換回普通容器的假設相匹配。

UNTREEIFY_THRESHOLD

在調整大小操作期間取消(分割)存儲庫的存儲計數閾值。

應小于TREEIFY_THRESHOLD,并最多6個網格與收縮檢測下去除。

MIN_TREEIFY_CAPACITY

最小的表容量,可為容器進行樹狀排列。(否則,如果在一個bin中有太多節點,表將被調整大小。)

至少為 4 * TREEIFY_THRESHOLD,以避免調整大小和樹化閾值之間的沖突。

Node

源碼

  • Node.java

基礎 hash 結點定義。

/**
?*?Basic?hash?bin?node,?used?for?most?entries.??(See?below?for
?*?TreeNode?subclass,?and?in?LinkedHashMap?for?its?Entry?subclass.)
?*/
static?class?Node<K,V>?implements?Map.Entry<K,V>?{
????final?int?hash;
????final?K?key;
????V?value;
????Node?next;
????Node(int?hash,?K?key,?V?value,?Node?next)?{this.hash?=?hash;this.key?=?key;this.value?=?value;this.next?=?next;
????}public?final?K?getKey()????????{?return?key;?}public?final?V?getValue()??????{?return?value;?}public?final?String?toString()?{?return?key?+?"="?+?value;?}public?final?int?hashCode()?{return?Objects.hashCode(key)?^?Objects.hashCode(value);
????}public?final?V?setValue(V?newValue)?{
????????V?oldValue?=?value;
????????value?=?newValue;return?oldValue;
????}public?final?boolean?equals(Object?o)?{//?快速判斷if?(o?==?this)return?true;//?類型判斷????if?(o?instanceof?Map.Entry)?{
????????????Map.Entry,?>?e?=?(Map.Entry,?>)o;if?(Objects.equals(key,?e.getKey())?&&
????????????????Objects.equals(value,?e.getValue()))return?true;
????????}return?false;
????}
}

個人理解

四個核心元素:

final?int?hash;?//?hash?值
final?K?key;????//?key
V?value;????//?value?值
Node?next;?//?下一個元素結點

hash 值的算法

hash 算法如下。

直接 key/value 的異或(^)。

Objects.hashCode(key)?^?Objects.hashCode(value);

其中 hashCode() 方法如下:

public?static?int?hashCode(Object?o)?{
????return?o?!=?null???o.hashCode()?:?0;
}

最后還是會調用對象本身的 hashCode() 算法。一般我們自己會定義。

靜態工具類

hash

static?final?int?hash(Object?key)?{
????int?h;
????return?(key?==?null)???0?:?(h?=?key.hashCode())?^?(h?>>>?16);
}

為什么這么設計?

  • jdk8 自帶解釋

計算key.hashCode(),并將(XORs)的高比特位分散到低比特位。

因為表使用的是power-of-two掩蔽,所以只在當前掩碼上方以位為單位變化的哈希總是會發生沖突。

(已知的例子中有一組浮點鍵,它們在小表中保存連續的整數。)

因此,我們應用了一種轉換,將高比特的影響向下傳播。

比特傳播的速度、效用和質量之間存在權衡。

因為許多常見的散列集已經合理分布(所以不要受益于傳播),因為我們用樹來處理大型的碰撞在垃圾箱,我們只是XOR一些改變以最便宜的方式來減少系統lossage,以及將最高位的影響,否則永遠不會因為指數計算中使用的表。

  • 知乎的解釋

這段代碼叫擾動函數

HashMap擴容之前的數組初始大小才16。所以這個散列值是不能直接拿來用的。

用之前還要先做對數組的長度取模運算,得到的余數才能用來訪問數組下標。

putVal 函數源碼

final?V?putVal(int?hash,?K?key,?V?value,?boolean?onlyIfAbsent,boolean?evict)?{
????????Node[]?tab;?Node?p;?int?n,?i;if?((tab?=?table)?==?null?||?(n?=?tab.length)?==?0)
????????????n?=?(tab?=?resize()).length;if?((p?=?tab[i?=?(n?-?1)?&?hash])?==?null)
????????????tab[i]?=?newNode(hash,?key,?value,?null);//...????
}

其中這一句 tab[i = (n - 1) & hash])

這一步就是在尋找桶的過程,就是上圖總數組,根據容量取如果容量是16 對hash值取低16位,那么下標范圍就在容量大小范圍內了。

這里也就解釋了為什么 hashmap 的大小需要為 2 的正整數冪,因為這樣(數組長度-1)正好相當于一個“低位掩碼”。

比如大小 16,則 (16-1) = 15 = 00000000 00000000 00001111(二進制);

????10100101?11000100?00100101
&?00000000?00000000?00001111
-------------------------------
????00000000?00000000?00000101????//高位全部歸零,只保留末四位

但是問題是,散列值分布再松散,要是只取最后幾位的話,碰撞也會很嚴重。

擾動函數的價值如下:

d49f8b649d3f6c831dd33d6c3347184e.png
擾動函數的價值

右位移16位,正好是32bit的一半,自己的高半區和低半區做異或,就是為了混合原始哈希碼的高位和低位,以此來加大低位的隨機性。

而且混合后的低位摻雜了高位的部分特征,這樣高位的信息也被變相保留下來。

優化哈希的原理介紹

comparable class

  • comparableClassFor()

獲取對象 x 的類,如果這個類實現了 class C implements Comparable 接口。

ps: 這個方法很有借鑒意義,可以做簡單的拓展。我們可以獲取任意接口泛型中的類型。

static?Class>?comparableClassFor(Object?x)?{
????if?(x?instanceof?Comparable)?{
????????Class>?c;?Type[]?ts,?as;?Type?t;?ParameterizedType?p;
????????if?((c?=?x.getClass())?==?String.class)?//?bypass?checksreturn?c;
????????if?((ts?=?c.getGenericInterfaces())?!=?null)?{
????????????for?(int?i?=?0;?i?????????????????if?(((t?=?ts[i])?instanceof?ParameterizedType)?&&
????????????????????((p?=?(ParameterizedType)t).getRawType()?==
?????????????????????Comparable.class)?&&
????????????????????(as?=?p.getActualTypeArguments())?!=?null?&&
????????????????????as.length?==?1?&&?as[0]?==?c)?//?type?arg?is?c
????????????????????return?c;
????????????}
????????}
????}
????return?null;
}

compareComparables()

獲取兩個可比較對象的比較結果。

@SuppressWarnings({"rawtypes","unchecked"})?//?for?cast?to?Comparable
static?int?compareComparables(Class>?kc,?Object?k,?Object?x)?{
????return?(x?==?null?||?x.getClass()?!=?kc???0?:
????????????((Comparable)k).compareTo(x));
}

tableSizeFor

獲取 2 的冪

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;
}
  • 被調用處
public?HashMap(int?initialCapacity,?float?loadFactor)?{
????//?check...
????this.loadFactor?=?loadFactor;
????this.threshold?=?tableSizeFor(initialCapacity);
}
  • 感想

emmm....為什么要這么寫?性能嗎?

簡單分析

當在實例化HashMap實例時,如果給定了initialCapacity,由于HashMap的capacity都是2的冪,因此這個方法用于找到大于等于initialCapacity的最小的2的冪(initialCapacity如果就是2的冪,則返回的還是這個數)。

  • 為什么要 -1

int n = cap - 1;

首先,為什么要對cap做減1操作。int n = cap - 1; 這是為了防止,cap已經是2的冪。如果cap已經是2的冪, 又沒有執行這個減1操作,則執行完后面的幾條無符號右移操作之后,返回的capacity將是這個cap的2倍。如果不懂,要看完后面的幾個無符號右移之后再回來看看。

下面看看這幾個無符號右移操作:

如果n這時為0了(經過了cap-1之后),則經過后面的幾次無符號右移依然是0,最后返回的capacity是1(最后有個n+1的操作)。

這里只討論n不等于0的情況。

  • 第一次位運算

n |= n >>> 1;

由于n不等于0,則n的二進制表示中總會有一bit為1,這時考慮最高位的1。

通過無符號右移1位,則將最高位的1右移了1位,再做或操作,使得n的二進制表示中與最高位的1緊鄰的右邊一位也為1,如000011xxxxxx。

其他依次類推

實例

比如 initialCapacity = 10;

表達式???????????????????????二進制
------------------------------------------------------????

initialCapacity?=?10;
int?n?=?9;??????????????????0000?1001
------------------------------------------------------????


n?|=?n?>>>?1;???????????????0000?1001
????????????????????????????0000?0100???(右移1位)?或運算
??????????????????????????=?0000?1101
------------------------------------------------------????

n?|=?n?>>>?2;???????????????0000?1101
????????????????????????????0000?0011???(右移2位)?或運算
??????????????????????????=?0000?1111
------------------------------------------------------????

n?|=?n?>>>?4;???????????????0000?1111
????????????????????????????0000?0000???(右移4位)?或運算
??????????????????????????=?0000?1111
------------------------------------------------------??

n?|=?n?>>>?8;???????????????0000?1111
????????????????????????????0000?0000???(右移8位)?或運算
??????????????????????????=?0000?1111
------------------------------------------------------??

n?|=?n?>>>?16;??????????????0000?1111
????????????????????????????0000?0000???(右移16位)?或運算
??????????????????????????=?0000?1111
------------------------------------------------------??

n = n+1;????????????????????0001 0000????結果:2^4 = 16;??????

put() 解釋

下面的內容出自美團博客 Java 8系列之重新認識HashMap

由于寫的非常好,此處就直接復制過來了。

流程圖解

HashMap的put方法執行過程可以通過下圖來理解,自己有興趣可以去對比源碼更清楚地研究學習。

1053b1fd3a6731b5e4a7f4f395642aaa.png
輸入圖片說明

①.判斷鍵值對數組table[i]是否為空或為null,否則執行resize()進行擴容;

②.根據鍵值key計算hash值得到插入的數組索引i,如果table[i]==null,直接新建節點添加,轉向⑥,如果table[i]不為空,轉向③;

③.判斷table[i]的首個元素是否和key一樣,如果相同直接覆蓋value,否則轉向④,這里的相同指的是hashCode以及equals;

④.判斷table[i] 是否為treeNode,即table[i] 是否是紅黑樹,如果是紅黑樹,則直接在樹中插入鍵值對,否則轉向⑤;

⑤.遍歷table[i],判斷鏈表長度是否大于8,大于8的話把鏈表轉換為紅黑樹,在紅黑樹中執行插入操作,否則進行鏈表的插入操作;遍歷過程中若發現key已經存在直接覆蓋value即可;

⑥.插入成功后,判斷實際存在的鍵值對數量size是否超多了最大容量threshold,如果超過,進行擴容。

方法源碼

public?V?put(K?key,?V?value)?{
????return?putVal(hash(key),?key,?value,?false,?true);
}
/**
?*?Implements?Map.put?and?related?methods
?*
?*?@param?hash?hash?for?key
?*?@param?key?the?key
?*?@param?value?the?value?to?put
?*?@param?onlyIfAbsent?if?true,?don't?change?existing?value
?*?@param?evict?if?false,?the?table?is?in?creation?mode.
?*?@return?previous?value,?or?null?if?none
?*/
final?V?putVal(int?hash,?K?key,?V?value,?boolean?onlyIfAbsent,boolean?evict)?{
????Node[]?tab;?Node?p;?int?n,?i;if?((tab?=?table)?==?null?||?(n?=?tab.length)?==?0)
????????n?=?(tab?=?resize()).length;if?((p?=?tab[i?=?(n?-?1)?&?hash])?==?null)
????????tab[i]?=?newNode(hash,?key,?value,?null);else?{
????????Node?e;?K?k;if?(p.hash?==?hash?&&
????????????((k?=?p.key)?==?key?||?(key?!=?null?&&?key.equals(k))))
????????????e?=?p;else?if?(p?instanceof?TreeNode)
????????????e?=?((TreeNode)p).putTreeVal(this,?tab,?hash,?key,?value);else?{for?(int?binCount?=?0;?;?++binCount)?{if?((e?=?p.next)?==?null)?{
????????????????????p.next?=?newNode(hash,?key,?value,?null);if?(binCount?>=?TREEIFY_THRESHOLD?-?1)?//?-1?for?1st
????????????????????????treeifyBin(tab,?hash);break;
????????????????}if?(e.hash?==?hash?&&
????????????????????((k?=?e.key)?==?key?||?(key?!=?null?&&?key.equals(k))))break;
????????????????p?=?e;
????????????}
????????}if?(e?!=?null)?{?//?existing?mapping?for?key
????????????V?oldValue?=?e.value;if?(!onlyIfAbsent?||?oldValue?==?null)
????????????????e.value?=?value;
????????????afterNodeAccess(e);return?oldValue;
????????}
????}
????++modCount;if?(++size?>?threshold)
????????resize();
????afterNodeInsertion(evict);return?null;
}

擴容機制

簡介

擴容(resize)就是重新計算容量,向HashMap對象里不停的添加元素,而HashMap對象內部的數組無法裝載更多的元素時,對象就需要擴大數組的長度,以便能裝入更多的元素。

當然Java里的數組是無法自動擴容的,方法是使用一個新的數組代替已有的容量小的數組,就像我們用一個小桶裝水,如果想裝更多的水,就得換大水桶。

JDK7 源碼

我們分析下resize()的源碼,鑒于JDK1.8融入了紅黑樹,較復雜,為了便于理解我們仍然使用JDK1.7的代碼,好理解一些,本質上區別不大,具體區別后文再說。

void?resize(int?newCapacity)?{???//傳入新的容量
????Entry[]?oldTable?=?table;????//引用擴容前的Entry數組
????int?oldCapacity?=?oldTable.length;?????????
????if?(oldCapacity?==?MAXIMUM_CAPACITY)?{??//擴容前的數組大小如果已經達到最大(2^30)了
????????threshold?=?Integer.MAX_VALUE;?//修改閾值為int的最大值(2^31-1),這樣以后就不會擴容了
????????return;
????}

????Entry[]?newTable?=?new?Entry[newCapacity];??//初始化一個新的Entry數組
????transfer(newTable);?????????????????????????//!!將數據轉移到新的Entry數組里
????table?=?newTable;???????????????????????????//HashMap的table屬性引用新的Entry數組
????threshold?=?(int)(newCapacity?*?loadFactor);//修改閾值
}

這里就是使用一個容量更大的數組來代替已有的容量小的數組,transfer() 方法將原有Entry數組的元素拷貝到新的Entry數組里。

void?transfer(Entry[]?newTable)?{
????Entry[]?src?=?table;???????????????????//src引用了舊的Entry數組
????int?newCapacity?=?newTable.length;
????for?(int?j?=?0;?j?//遍歷舊的Entry數組
????????Entry?e?=?src[j];?????????????//取得舊Entry數組的每個元素if?(e?!=?null)?{
????????????src[j]?=?null;//釋放舊Entry數組的對象引用(for循環后,舊的Entry數組不再引用任何對象)do?{
????????????????Entry?next?=?e.next;int?i?=?indexFor(e.hash,?newCapacity);?//!!重新計算每個元素在數組中的位置
????????????????e.next?=?newTable[i];?//標記[1]
????????????????newTable[i]?=?e;??????//將元素放在數組上
????????????????e?=?next;?????????????//訪問下一個Entry鏈上的元素
????????????}?while?(e?!=?null);
????????}
????}
}

newTable[i]的引用賦給了e.next,也就是使用了單鏈表的頭插入方式,同一位置上新元素總會被放在鏈表的頭部位置;

這樣先放在一個索引上的元素終會被放到Entry鏈的尾部(如果發生了hash沖突的話),這一點和Jdk1.8有區別,下文詳解。

在舊數組中同一條Entry鏈上的元素,通過重新計算索引位置后,有可能被放到了新數組的不同位置上。

案例

下面舉個例子說明下擴容過程。假設了我們的hash算法就是簡單的用key mod 一下表的大小(也就是數組的長度)。

其中的哈希桶數組table的size=2, 所以key = 3、7、5,put順序依次為 5、7、3。

在mod 2以后都沖突在table[1]這里了。

這里假設負載因子 loadFactor=1,即當鍵值對的實際大小size 大于 table的實際大小時進行擴容。

接下來的三個步驟是哈希桶數組 resize成4,然后所有的Node重新rehash的過程。

a22b6fcad6244b3ec4bc1a782e9d8c6b.png
輸入圖片說明

Jdk8 優化

經過觀測可以發現,我們使用的是2次冪的擴展(指長度擴為原來2倍),所以,元素的位置要么是在原位置,要么是在原位置再移動2次冪的位置。

看下圖可以明白這句話的意思,n為table的長度,圖(a)表示擴容前的key1和key2兩種key確定索引位置的示例,

圖(b)表示擴容后key1和key2兩種key確定索引位置的示例,其中hash1是key1對應的哈希與高位運算結果。

ab12193c896345a925b6b7684e2b8f8b.png
位運算

元素在重新計算hash之后,因為n變為2倍,那么n-1的mask范圍在高位多1bit(紅色),因此新的index就會發生這樣的變化:

4683c441f49f598db4c1acf00f99ac55.png
index

因此,我們在擴充HashMap的時候,不需要像JDK1.7的實現那樣重新計算hash,只需要看看原來的hash值新增的那個bit是1還是0就好了,是0的話索引沒變,是1的話索引變成“原索引+oldCap”,可以看看下圖為16擴充為32的resize示意圖:

5fe443bd2cdd30b633caedd319c2cd00.png
rehash

這個設計確實非常的巧妙,既省去了重新計算hash值的時間,而且同時,由于新增的1bit是0還是1可以認為是隨機的,因此resize的過程,均勻的把之前的沖突的節點分散到新的bucket了。

這一塊就是JDK1.8新增的優化點。

有一點注意區別,JDK1.7中rehash的時候,舊鏈表遷移新鏈表的時候,如果在新表的數組索引位置相同,則鏈表元素會倒置,但是從上圖可以看出,JDK1.8不會倒置。

JDK8 源碼

有興趣的同學可以研究下JDK1.8的resize源碼,寫的很贊:

/**
?*?Initializes?or?doubles?table?size.??If?null,?allocates?in
?*?accord?with?initial?capacity?target?held?in?field?threshold.
?*?Otherwise,?because?we?are?using?power-of-two?expansion,?the
?*?elements?from?each?bin?must?either?stay?at?same?index,?or?move
?*?with?a?power?of?two?offset?in?the?new?table.
?*
?*?@return?the?table
?*/
final?Node[]?resize()?{
????Node[]?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)??????????????????oldCap?>=?DEFAULT_INITIAL_CAPACITY)
????????????newThr?=?oldThr?<1;?//?double?threshold
????}else?if?(oldThr?>?0)?//?initial?capacity?was?placed?in?threshold
????????newCap?=?oldThr;else?{???????????????//?zero?initial?threshold?signifies?using?defaults
????????newCap?=?DEFAULT_INITIAL_CAPACITY;
????????newThr?=?(int)(DEFAULT_LOAD_FACTOR?*?DEFAULT_INITIAL_CAPACITY);
????}if?(newThr?==?0)?{float?ft?=?(float)newCap?*?loadFactor;
????????newThr?=?(newCap?float)MAXIMUM_CAPACITY??
??????????????????(int)ft?:?Integer.MAX_VALUE);
????}
????threshold?=?newThr;@SuppressWarnings({"rawtypes","unchecked"})
????????Node[]?newTab?=?(Node[])new?Node[newCap];
????table?=?newTab;if?(oldTab?!=?null)?{for?(int?j?=?0;?j?????????????Node?e;if?((e?=?oldTab[j])?!=?null)?{
????????????????oldTab[j]?=?null;if?(e.next?==?null)
????????????????????newTab[e.hash?&?(newCap?-?1)]?=?e;else?if?(e?instanceof?TreeNode)
????????????????????((TreeNode)e).split(this,?newTab,?j,?oldCap);else?{?//?preserve?order
????????????????????Node?loHead?=?null,?loTail?=?null;
????????????????????Node?hiHead?=?null,?hiTail?=?null;
????????????????????Node?next;do?{
????????????????????????next?=?e.next;if?((e.hash?&?oldCap)?==?0)?{if?(loTail?==?null)
????????????????????????????????loHead?=?e;else
????????????????????????????????loTail.next?=?e;
????????????????????????????loTail?=?e;
????????????????????????}else?{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;
}

小結

如果你已經通讀全文,那么你已經非常厲害了。

其實第一遍沒有徹底理解也沒有關系,知道 HashMap 有一個 reHash 的過程就行,類似于 ArrayList 的 resize。

下一節我們將一起學習下自己手寫實現一個漸進式 rehash 的 HashMap,感興趣的可以關注一下,便于實時接收最新內容。

覺得本文對你有幫助的話,歡迎點贊評論收藏轉發一波。你的鼓勵,是我最大的動力~

不知道你有哪些收獲呢?或者有其他更多的想法,歡迎留言區和我一起討論,期待與你的思考相遇。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/533696.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/533696.shtml
英文地址,請注明出處:http://en.pswp.cn/news/533696.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

南京高中計算機老師,南京市教育局召開中小學教師信息技術應用能力提升工程2.0市級專家組工作會議...

2021年3月2日上午&#xff0c;南京市中小學教師信息技術應用能力提升工程2.0市級專家組工作會議在雨花臺區教師發展中心召開。市教育局副局長祁壽東出席會議并講話&#xff0c;市教研室、教科所、電教館、教師發展學院主要負責同志&#xff0c;市級專家團隊成員及各區教師發展中…

python計算執行時間的函數_[python] 統計函數運行時間

第一種&#xff1a; import time def time_me(fn): #fn 是要修飾/修改 的函數 def _wrapper(*args, **kwargs): #這個 _wrapper(*args, **kwargs) 則代指fn, *args 代表一般變量參數&#xff0c; **kwargs代表 字典&#xff0c;哈希等參數 start time.perf_counter() fn(*args…

arthas 排查內存溢出_Java 應用線上問題排查思路、常用工具小結

前言本文總結了一些常見的線上應急現象和對應排查步驟和工具。分享的主要目的是想讓對線上問題接觸少的同學有個預先認知&#xff0c;免得在遇到實際問題時手忙腳亂。畢竟作者自己也是從手忙腳亂時走過來的。只不過這里先提示一下。在線上應急過程中要記住&#xff0c;只有一個…

計算機個性化定制服務課題,服務網絡的構建與面向增量式需求的動態定制方法-計算機科學與技術專業論文.docx...

服務網絡的構建與面向增量式需求的動態定制方法-計算機科學與技術專業論文Classified Index: TP315 U.D.C: 681.3Dissertation for the Master’s Degree in EngineeringSERVICE NETWORK CONSTRUCTION AND DYNAMIC CUSTOMIZATION METHOD FOR SUBJECTIVE CHANGES OF CUSTOMER RE…

flutter listview 滾動到指定位置_Flutter 布局原理及實戰

1. Flutter UI架構Flutter將視圖數據抽象成為三個部分&#xff0c;即Widget樹、Element樹和RenderObject樹。Widget樹&#xff1a;控件的配置信息&#xff0c;不涉及渲染&#xff0c;更新代價極低。RenderObject樹&#xff1a;真正的UI渲染樹&#xff0c;負責渲染UI&#xff0c…

計算機的屏幕約是16平方分米嗎,小明的臥室有16平方分米對不對

小明的臥室有16平方分米對不對不對&#xff0c;應該是16平方米不對錯! 16平方分米太小了不對&#xff0c;那么小怎么可能住人。不正確應該是16平方米xiao ming de wo shi you 1 6 ping fang fen mi dui bu dui32平方分米涂上每平方分米的96克油漆,需要幾克32平方分米需要油漆30…

python引用傳遞_python 是值傳遞還是引用傳遞 知乎

展開全部 那要看數據類型了&#xff0c;21135261int&#xff0c;float&#xff0c;str這種就是傳值&#xff0c;list&#xff0c;dict&#xff0c;類的實例&#xff0c;自定義對象都是穿4102引用。 下面1653是示例代碼&#xff1a;def change(int1,float1,str1,dict1,obj1,list…

雷神開機logo更改_國產外星人雷神再發新品 911MT逐影者RTX2060光追游戲本評測

隨著NVIDIA發布了筆記本20系顯卡之后&#xff0c;宣示著全民進入了“RTX光線追蹤時代”&#xff0c;各種新款的游戲也紛紛宣布支持“光線追蹤”技術來吸引更多的玩家&#xff0c;似乎現在游戲本上沒有個“RTX”貼紙就已經不好意思跟別人打招呼了。說到2019年的RTX新品&#xff…

AJAX框架衣柜內部布局,?最合理的衣柜內部布局解析,3大細節不容小覷

時常有業主或者朋友問小輕&#xff0c;最合理的衣柜內部布局應該是怎樣的&#xff0c;確實這對于非業內人士一般都是不太清楚的&#xff0c;即使有的朋友已經有了豐富的生活經驗&#xff0c;甚至是業內人士也不一定對此完全了解。那么到底最合理的衣柜內部布局是怎樣的呢&#…

python爬取數據保存為csv時生成編號_將爬取到到數據以CSV格式存儲

CSV文件存儲 CSV&#xff0c;全稱為Comma-Separated Values&#xff0c;中文可以叫做逗號分隔值或字符分隔值&#xff0c;其文件以純文本形式存儲表格數據。該文件是一個字符序列&#xff0c;可以由任意數目的記錄組成&#xff0c;記錄間以某種換行符分隔。每條記錄由字段組成&…

博達3956交換機配置手冊_網絡設備維保淺談之交換機維保

隨著信息化的飛速發展&#xff0c;交換機作為信息流通的承載者&#xff0c;是應用最為廣泛的網絡設備之一&#xff0c;其作用不言而喻。因此&#xff0c;在日產使用中&#xff0c;要注意交換機這種核心的設備的維護與保養&#xff0c;以免引發故障。交換機運維需要注意哪些問題…

java cas原理_Java并發之原子變量及CAS算法-上篇

Java并發之原子變量及CAS算法-上篇編輯?概述本文主要講在Java并發編程的時候&#xff0c;如果保證變量的原子性&#xff0c;在JDK提供的類中是怎么保證變量原子性的呢&#xff1f;。對應Java中的包是&#xff1a;java.util.concurrent.atomic包下。因為涉及到了CAS算法&#x…

node ajax validator,使用validator.js對字符串數據進行驗證

validator.js是一個對字符串進行數據驗證和過濾的工具庫&#xff0c;同時支持Node端和瀏覽器端&#xff0c;github地址是https://github.com/chriso/validator.js主要API如下&#xff1a;驗證APIcontains(str, seed)驗證str中是否含有seedequals(str, comparison)驗證是否相等i…

css span 右端對齊_CSS標準文檔流

web頁面的制作&#xff0c;是個“流”&#xff0c;像水流一樣&#xff0c;必須從上往下&#xff0c;一點點的編織&#xff0c;不像畫畫&#xff0c;可以這個地方畫一個&#xff0c;另一個地方畫一個&#xff0c;隨意而為。標準文檔流的一些微觀現象1. 空白折疊現象1)標簽與標簽…

composer升級_Composer 使用姿勢與 Lumen 升級指南

Composer 使用姿勢這里主要說說 composer.json 和 composer.lock 文件的作用。composer.jsoncomposer.json 文件包含了項目的依賴和其它的一些元數據&#xff0c;使用 JSON format 編寫。當初次調用 composer install 時&#xff0c;Composer 會根據 composer.json 文件&#x…

服務器間傳文件$d,基于OpenSSH+WinSCP完成Windows服務器之間的文件傳輸

背景經常會遇到在不同服務器之間傳輸文件&#xff0c;Linux和Linux之間用命令rsync&#xff0c; windows和linux之間普遍是有圖形化界面的ftp軟件&#xff0c;老黃平時用的比較多的是FileZilla。Windows和Windows之間的話&#xff0c;90%都是在一臺機器復制&#xff0c;到另一臺…

dbgrideh 為什么只一行_Mysql性能優化:為什么count(*)這么慢?

導讀在開發中一定會用到統計一張表的行數&#xff0c;比如一個交易系統&#xff0c;老板會讓你每天生成一個報表&#xff0c;這些統計信息少不了sql中的count函數。但是隨著記錄越來越多&#xff0c;查詢的速度會越來越慢&#xff0c;為什么會這樣呢&#xff1f;Mysql內部到底是…

jmeter 高并發測試報告_JMeter分布式測試

一、為什么要使用分布式測試按照一般的壓力機配置&#xff0c;jmeter的GUI模式下(Windows)&#xff0c;最多支持300左右的模擬請求線程&#xff0c;再大的話&#xff0c;容易造成卡頓、無響應等情況&#xff0c;這是限于jmeter其本身的機制和硬件配置。有時候為了盡量模擬業務場…

登陸攔截攔截ajax,過濾器實現登錄攔截需要注意的問題(AJAX請求的處理)

1.問題描述&#xff1a;最近自己在寫demo時遇到一個問題&#xff0c;在ajax請求時用Filter做登錄攔截&#xff0c;結果頁面不跳轉(Ajax是不能做轉發和重定向的)、、、、最終的最終在同事zt的提示下&#xff0c;恍然大悟&#xff0c;雖然很基本的問題&#xff0c;但也糾結了好久…

半圓陰影_六年級數學:怎么求陰影部分面積?正方形與半圓,割補法常考題

歡迎您來到方老師數學課堂&#xff0c;請點擊上方藍色字體&#xff0c;添加關注。所有的視頻內容&#xff0c;全部免費&#xff0c;請大家放心關注&#xff0c;放心訂閱。六年級數學&#xff1a;怎么求陰影部分面積&#xff1f;正方形與半圓&#xff0c;割補法常考題。大家先在…