相信你們在面試的過程當中,常常會被問到一個這樣的問題:"你瞭解hashmap的底層原理嗎?",大多數初級人員或許只是瞭解它的底層數據結構是什麼,基本的做用是什麼,可是一旦問到擴容過程,put的過程,紅黑樹的變色和翻轉(本篇不支持),你們不免就沒法從容面對。node
那咱們就一塊去看下hashmap的底層源碼是什麼樣子的~~面試
開始以前請你們思考一下,咱們常見的數據結構有哪些?典型的表明又有哪些呢?算法
01 常見數據機構數組
咱們比較熟悉的應該是這幾種:數組,鏈表(單向和雙向),樹形,圖形數據結構
典型的表明:app
數組:相似以下,典型表明是Arraylist和Vectorless
鏈表:典型表明是LinkedListide
雙向:this
單向3d
紅黑樹:典型表明是hashmap(jdk1.8以前是數組加鏈表,1.8以後又增長了紅黑樹)
hashmap的結構組成是 :數組+鏈表+紅黑樹
02 HashMap的常見參數
咱們能夠先考慮這麼幾個問題:
/** * The default initial capacity - MUST be a power of two. */static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
DEFAULT_INITIAL_CAPACITY參數就是默認的大小值,就是16
/** * 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;
MAXIMUM_CAPACITY就是數組的上限值
/** * The load factor used when none specified in constructor. */static final float DEFAULT_LOAD_FACTOR = 0.75f;
DEFAULT_LOAD_FACTOR加載因子,也就是說當容器使用了16*0.75=12,的時候,就開始擴容。
這也就意味着它們之間能夠進行轉換,根據不一樣的場景選擇不一樣的存儲結構,,這纔是合理的,那麼何時纔會轉換呢?
鏈表轉紅黑樹:
/** * 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;
根據英文的意思就能夠看出這是用於紅黑樹的,意味着當鏈表的長度>=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;
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;
MIN_TREEIFY_CAPACITY,是樹的節點最小數量,依據就是 4 * 數組長度(16)=64
03put的過程
明白了上面幾個參數的意義就是,咱們就開始嘗試去看一下源碼,咱們就以put方法爲例子,看看究竟是怎麼個意思。
咱們看到 putVal(hash(key), key, value, false, true);
這有4個參數,前兩個分別是key(key的hash值),value,第三個表明遇到重複值是否要覆蓋,fasle是覆蓋,最後一個參數是指插入結束後要不要建立新的模式,false表明是(可參考英文註解)
此時你或許會疑問,爲何要對key進行hash化,這涉及到hash算法,你們想一下,map的數組長度默認是16,並且map是無序的,也就意味着要在下標爲0-15中隨機產生並且人家更要考慮的均勻性,就是0-15不只要隨機,還要每一個數字都要雨露均沾。目的就是儘可能讓這些數字產生的更加公平均勻。
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}
至於如何雨露均沾,日後看。
下面我會摘取源碼的一塊塊的進行截取解讀,建議你們比着源碼來看
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;
看一下前幾行的代碼,首先定義了兩個數組tab和p,以及兩個int變量,n和i,咱們繼續往下看就知道做用了。
if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length;
table是常量,記錄了map的數據,咱們每插入一條數據,table就會多一條記錄下來。
這塊代碼就是初始化,看看是否是第一次添加數據,若是是就經過resize()方法給tab初始化,並由變量n記錄當前長度,此時咱們已經看到了兩個變量的做用。
Resize()方法的做用有2個,初始化數組和擴容,最後咱們會一塊兒看一下,此處先認識有這樣一個操做 。
if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null);else {
數組下標值是(n -1) & hash 這行代碼來獲取的,爲何能實現雨露均沾,這個就涉及二進制的&運算,具體請關注往期文章
else後面的代碼我並無粘過來,爲何?由於這就涉及到一個常見面試點:hash碰撞
什麼是hash碰撞?就是產生的hash值重複了,既然是產生1-16,那麼重複的機率仍是很高的,沒有重複,就按照上面所寫的,我新建一個節點就行了,可是重複了呢?就是else的內容了,在此以前咱們先明確思路,再去看代碼就簡單的很了
1,要追加的地方,自己尚未鏈表,要添加的是第一個
2,我要追加的可能不是鏈表,多是紅黑樹,那我直接轉變成樹的節點就好
3,後面有鏈表,那就須要我不停的去遍歷,而後找到合適的位置,可是由於是新添加節點,咱們還要考慮鏈表長度達到了8,就要轉變成紅黑樹。
else { Node<K,V> e; K k;//狀況1 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p;//狀況2 轉變爲樹的節點 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);//狀況3 循環查找鏈表查找位置 else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); //若是長度大於等於8了,要變成紅黑樹 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;
不知道根據上面的註釋你們是否有了理解
++modCount;if (++size > threshold) resize();
最後幾行,由於咱們隨時要記錄長度,準備擴容,最後幾行的目的就是來判斷是否要擴容的.。
擴容,resize()也是常常被問到的,咱們也去看下,仍是先說思路;
1,擴容就是達到了指定長度後,每次擴容2倍,16會變成32,可是若是容器自己超過了最大限制,就無法擴容了
2,擴容後會對現有數據從新排序,爲何呢?和生活同樣,咱們住的空間大了,那咱們的行李用品也要搬一些到新空間,別顯得那麼擁擠,但又不是所有搬走,會選一部分搬,至於怎麼選呢?就體現出二進制算法的精妙之處了,具體如何實現,日後看
Node<K,V>[] oldTab = table;int oldCap = (oldTab == null) ? 0 : oldTab.length;int oldThr = threshold;int newCap, newThr = 0;//oldcap爲0說明是初始化,不要忘記resize的做用是初始化和擴容if (oldCap > 0) { //長度超出了最大值,沒法擴容 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; }//擴容後的值不超過最大值,就擴容2倍 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;
剛纔也提到擴容後從新排序的,感興趣的本身去研究一下,下面是代碼
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;
可是要提醒的是,剛纔說到得巧妙之處是
if ((e.hash & oldCap) == 0
此處的&運算就是精密之處,經過二進制運算來比較現有的與原來的不一樣,會產生0和1,0就留下,1就移走,移走後的位置下標是 原先位置+擴容長度
纔能有限,源碼就帶你們看到這,那咱們進入最後一個環節,問幾個問題,看看可否回答?
1,hashmap的底層原理,數據結構是什麼?
底層是哈希表(數組加鏈表),1.8以後引入了紅黑樹
2,能說一下hashmap的put過程嗎?
1>傳入key-value,並根據key求出哈希值,用於計算下標2>查看是否衝突,不衝突就裝入容器中,3>衝突就追加到鏈表中,而且要查看是否達到鏈表閾值,達到要轉換成紅黑樹4>查看節點是否重複,重複就覆蓋5>查看容器是否要擴容
3,hashmap是如何得到下標的?
(n-1) & hash. 原理是高16位不變,與低16位作與或運算
4,能說下擴容嗎?擴容後部分數組位置確定要變化,變成什麼了呢?
1>判斷當前容量或擴容後的容量是否超出最大值,超出則沒法擴容,不然擴容會增長2倍2>從新遍歷數組,而後將部分移動到新位置(注意:resize還有初始化操做,若是記錄的table常量是空就初始化)位置就是原有位置+擴容數量
5,hashmap中的鏈表太長,查找時間複雜度可能會達到0(n),如何解決?
引入紅黑樹就是爲了解決這個問題
原做者:歸行-泰然
原文連接: 沒想到你是這樣HashMap!!
原出處:公衆號
侵刪