HashMap原始碼淺析(一)—— 建立物件與新增元素
本文屬於個人隨筆,純原創,轉載請註明出處
最近面試,屢屢被問道HashMap的實現,回答得總是不太流暢。回頭想想,HashMap作為平時用得最多的一個集合框架類,我對其其內部實現確實知之甚少。這有點說不過去,於是把原始碼搬出來,嘗試做個簡單分析,如有謬誤,還請指出,謝謝!
(本文所示原始碼均來源於JDK1.8.0_131)
首先翻譯一下類宣告前的一大段註釋吧,只提取關鍵資訊:
1、HashMap是基於雜湊表的Map介面實現類(間接實現:直接繼承自AbstractMap,而AbstractMap實現了Map介面);
2、與HashTable相比,HashMap有兩個區別:其一是HashMap執行緒不安全,其二是HashMap允許鍵為null;
3、鑑於其內部實現是雜湊表,HashMap無法保證內部元素的順序始終不變;
4、影響HashMap讀寫效能的兩個要素:初始容量與載入因子;前者確定HashMap物件建立初期的容量,後者用於HashMap判斷是否達到需要擴容的臨界值(容量*載入因子);如果達到臨界值,HashMap會重建內部資料結構,以獲得較之前相比約兩倍的容量;
5、載入因子預設0.75,以獲得性能與容量的平衡,過高可能會導致讀寫耗時增加、過低可能會浪費過多記憶體;
6、HashMap的迭代器Iterator內建fail-fast機制,不允許在遍歷過程中更改內部資料結構;
7、儘管該類在JDK 1.2開始就有了,但是直到JDK 1.8,該類還進行過大改動,這也從側面反映了這個類的使用範圍之廣,以至於一丁點的效能提升都顯得格為重要;
首先還是來看構造方法:
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); } public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted } public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); }
四個過載的構造方法,一一說明:
1、空參:載入因子預設0.75,沒有指定初始容量;
2、一個整形引數:指定初始容量、載入因子預設0.75;
3、兩個引數:指定初始容量和載入因子;
4、一個Map型別引數:載入因子預設0.75,並呼叫putMapEntries將引數中的鍵值對新增到HashMap中;
其中,前三個構造方法執行完成後都沒有建立用於存放鍵值對的容器,而第四個構造方法也只是在putVal的方法裡才會呼叫到resize()並建立Node<K,V>陣列;
這裡插一句,tableSizeFor這個方法用於判斷傳入的初始容量是否合法,寫得很牛逼,具體演算法大家直接嘗試看原始碼吧:
/**
* Returns a power of two size for the given target capacity.
*/
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;
}
在兩個引數的構造方法中,我們注意到:
this.threshold = tableSizeFor(initialCapacity);
按照字面意思,這裡的threshold應該存放載入因子乘以容量的臨界值,為何在此存的是初始容量的校準值(姑且這麼叫吧,就是通過tableSizeFor方法校準過的返回值)?先表明態度——這不是bug,至於為啥,設為疑問A,稍後自會分曉;
既然構造方法本身都不會建立容器,那麼一定和第四個構造方法一樣,我們的容器一定就是在新增第一個元素的時候建立的,來到最常用的put(K key, V value)方法:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
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;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> 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<K,V>)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;
}
和上方的第四個構造方法一樣,會呼叫到final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)方法中,假設這個HashMap在構造完成後立即進行了put元素的操作,我們可以將構造方法的值直接代入,檢視程式碼走向:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
n = (tab = resize()).length;
i = (n - 1) & hash;
tab[i] = newNode(hash, key, value, null);
++modCount;
if (++size > threshold)
resize();
return null;
}
簡化後的程式碼如上,我們可以看到,首先會執行resize()方法:
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; // 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 < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> 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<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> 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;
}
看到這裡,可以解答一下之前的疑問A了:
將初始容量存入threshold成員變數是為了在resize()方法中做判斷,再將其賦值與newCap變數,用於建立容器時使用,說白了,就是一個記錄臨時變數的作用。
好了,回來繼續看resize()方法,同樣我們按照之前的邏輯將方法簡化:
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
newCap = oldThr;
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
return newTab;
}
可以看出來,初始化容器其實並沒有做太多事情,主要是一些變數間的賦值和一個Node陣列的建立,而這個Node陣列,則是HashMap的內部資料結構;
我們從而得到結論1:HashMap是通過內部維護一個鍵值對陣列來實現的。
基於結論1,我們肯定會問,這跟雜湊有啥關係?
別急,我們只是看了resize()方法,而呼叫它的putVal方法我們還沒看完,於是我們回到putVal方法,來看看resize之後又做了什麼:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
n = (tab = resize()).length;
i = (n - 1) & hash;
tab[i] = newNode(hash, key, value, null);
++modCount;
if (++size > threshold)
resize();
return null;
}
關鍵程式碼出現了:
首先,陣列的下標i是通過陣列長度-1再位與上hash值而來;
其次,建立一個Node物件存放鍵值對於雜湊值,並將其賦值給tab[i]。如果說HashMap內部是一個數組,那麼往這個數組裡面的某個元素賦值,則就是往HashMap裡面新增元素的關鍵所在了;
不急,一個一個來。
首先,我們可能需要回頭看下這個整形引數hash的來歷:
翻遍整個HashMap.java,我們看到呼叫putVal並傳入的第一個引數,始終都是通過hash(key)獲得的;
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
再次宣告,本文程式碼基於JDK1.8.0_131,每個版本的方法實現可能不一樣。不過我們暫時不深入瞭解這個,不然就沒完沒了了,我們只需要知道,putVal方法的第一引數是通過HashMap內部的hash方法,對需要put的key進行雜湊運算得到的。
那麼利用運算得到的雜湊值hash位與上鍵值對陣列的最大下標(length - 1)所得的數值i,就是這次putVal操作需要新增的鍵值對存放在內部陣列的位置;
如果每次新增元素的操作都如我簡化後的程式碼這樣執行,那麼這個HashMap也就不會那麼備受推崇了,甚至你會覺得求hash變得沒那麼必要。別急,作為Map的實現類,乃至整個集合框架內使用頻率最高的HashMap,絕對沒那麼簡單。
這一篇先寫到這,本來打算一口氣寫完,不過感覺這樣會很亂,也很冗長。不如在此停頓一下,預計明天再開一篇,跟下putVal的其他幾個if 、else if;
相關推薦
HashMap原始碼淺析(一)—— 建立物件與新增元素
本文屬於個人隨筆,純原創,轉載請註明出處 最近面試,屢屢被問道HashMap的實現,回答得總是不太流暢。回頭想想,HashMap作為平時用得最多的一個集合框架類,我對其其內部實現確實知之甚少。這有點說不過去,於是把原始碼搬出來,嘗試做個簡單分析,如有謬誤,還請指出,謝謝!
Android Hook框架adbi原始碼淺析(一)
adbi(The Android Dynamic Binary Instrumentation Toolkit)是一個Android平臺通用hook框架,基於動態庫注入與inline hook技術實現。該框架由兩個主要模組構成,1.hijack負責將動態庫注入到目標程序;2.libbase提供動態庫本身,它實
Android廣播機制實現原始碼淺析(一)
Android系統的廣播機制應用非常的廣泛,是一種方便快捷的程序間通訊的方式。同時它也有一些很有特殊的使用方式,比如它的兩種註冊方式,三種類型的廣播等,這些充斥在整個系統框架中,那麼為了用好廣播,很有必要對其原始碼進行分析,從而避免一些低階失誤。 本文將對整個廣播機制涉及到
tomcat原始碼淺析(一)之 架構
tomcat程式碼看似很龐大,但從結構上看卻很清晰和簡單,它主要由一堆元件組成,如Server、Service、Connector等,並基於JMX管理這些元件,另外實現以上介面的元件也實現了代表生存期的介面Lifecycle,使其元件履行
zookeeper原始碼淺析(一)
1.基本架構 2.ZAB協議 ZooKeeper並沒有完全採用Paxos演算法,而是使用了一種稱為ZooKeeper Atomic Broadcast(ZAB,zookeeper原子訊息廣播協議)的協議作為其資料一致性的核心演算法。 &n
原始碼分析:Android Okhttp原始碼淺析(一)
Okhttp的呼叫 分析原始碼,我們都需要通過呼叫來檢視原始碼的每一步都做了什麼事情。 看下非同步呼叫 OkHttpClient client = new OkHttpClient(); Request request = n
JavaScript基礎——面向物件的程式設計(一)建立物件的幾種方式總結
簡介 面向物件(Object-Oriented, OO)的語言有一個標誌,那就是它們都有類的概念,而通過類可以建立任意多個具有相同屬性和方法的物件。前面提到過,ECMAScript中沒有類的概念,因此它的物件也與基於類的語言中的物件有所不同。 ECMA-262把物件定義為:
Zookeeper客戶端原始碼分析(一)建立連線
本文基於zookeeper-3.4.14,由於zookeeper的很多構造方法都是呼叫的另一個構造方法,所以分析程式碼的時候直接分
Volley原始碼解析(一)——傳送請求與結束請求
Volley是一個Android HTTP庫,只支援非同步方式。 傳送請求樣例 final TextView mTextView = (TextView) findViewById(R.id.text); ... // Instantiate
林大媽的JavaScript進階知識(一):物件與記憶體
JavaScript中的基本資料型別 在JS中,有6種基本資料型別: string number boolean null undefined Symbol(ES6) 除去這六種基本資料型別以外,其他的所有變數資料型別都是Object。基本型別的操作在JS底層中是這樣實現的: // 1. 申請一塊記憶體,
一篇文章徹底讀懂HashMap之HashMap原始碼解析(上)
就身邊同學的經歷來看,HashMap是求職面試中名副其實的“明星”,基本上每一加公司的面試多多少少都有問到HashMap的底層實現原理、原始碼等相關問題。 在秋招面試準備過程中,博主閱讀過很多關於HashMap原始碼分析的文章,漫長的拼湊式閱讀之後,博主沒有看到過
Java容器——HashMap(Java8)原始碼解析(一)
一 概述 HashMap是最常用的Java資料結構之一,是一個具有常數級別的存取速率的高效容器。相對於List,Set等,結構相對複雜,本篇我們先對HashMap的做一個基本說明,對組成元素和構造方法進行介紹。 二 繼承關係 首先看HashMap的繼承關係,比較簡單,實現了Map和序列化
java程式碼優化(一)——建立和銷燬物件
用靜態工廠方法代替構造器 準備知識 自動裝箱:從基本資料型別轉換成包裝型別。 自動拆箱:從包裝型別轉換成基本資料型別。 包裝類: number(數字型別) Byte(byte) Short(short) Integer(int) Long(long) Dou
Rxjava2原始碼分析(一):Flowable的建立和基本使用過程分析
本文用於記錄一下自己學習Rxjava2原始碼的過程。首先是最簡單的一個使用方法(未做執行緒切換),用來學習Flowable的建立和使用。Flowable .create(new FlowableOnSubscribe<Object>() {
物件池commons-pool框架的研究以及原始碼分析(一)
物件池是一個物件集合,用於將建立好的物件存在該集合中,當需要使用池中的物件時,再從池中取出,恰當地使用物件池可以有效減少物件生成和初始化時的消耗,提高系統的執行效率。另外,利用物件池還可以對物件的狀態做一定的維護,確保物件是可用的,提高程式的健壯性。注意:物件池技術
seajs原始碼分析-執行機制淺析(一)
前端技術發展簡直是日新月異,隨著angularjs,vuejs,reactjs等等這些框架的不斷興起,轉眼間jquery,seajs,Backbone這些框架已經成了清朝的框架了,再加上es6本身對於模組化的支援,也許,seajs模組化在將來的某天可能會徹底成為
Python 原始碼剖析(一)【python物件】
處於研究python記憶體釋放問題,在閱讀部分python原始碼,順便記錄下所得。 (基於《python原始碼剖析》(v2.4.1)與 python原始碼(v2.7.6)) 先列下總結: python 中一切皆為物件,所以會先講明白pyth
JS面向物件實戰(一)——建立一個新的函式物件的兩種習慣
JavaScript面向物件(一)——建立一個新的函式物件的兩種習慣 工作中可能習慣建立一個函式物件的方式,管理一個模組。那麼針對建立一個函式物件,一般有兩種習慣方式:偏向鏈式程式設計;偏向類 不能汙染函式祖先Function。 我們如果想新增一個方法
面向物件程式設計(一)建立類,例項化,屬性引用
在python中,用變量表示特徵,用函式表示技能,因而類是變數與函式的結合體,物件是變數與方法(指向類的函式)的結合體。 class 類名:#定義一個類 類體 類有兩種作用:屬性引用和例項化 t=類名()#例項化 類名加括號就是例項化,會自動觸發__in
Qt整體框架淺析(一) -物件模型
最近在膠著於QT的框架以及其核心, 本文作為學習的總結,如果有誤,望指正。 首先介紹一下Qt的整體框架,Qt作為一個GUI的解決方案,其被設計為基於面向物件,跨平臺,並直接與底層介面的framework,下圖為主要的層次結構: 下邊將從物件模型,事件機制