位運算操做是由處理器支持的底層操做,底層硬件只支持01這樣的數字,所以位運算運行速度很快。儘管現代計算機處理器擁有了更長的指令流水線和更優的架構設計,使得加法和乘法運算幾乎與位運算同樣快,可是位運算消耗更少的資源。經常使用的位運算以下:html
位與 & (1&1=1 1&0=0 0&0=0)java
位或 | (1|1=1 1|0=1 0|0=0)web
位非 ~ ( ~1=0 ~0=1)redis
位異或 ^ (1^1=0 1^0=1 0^0=0)算法
有符號右移 >> 在執行右移操做時,若參與運算的數字爲正數,則在高位補0;若爲負數,則在高位補1。shell
無符號右移 >>> 不管參與運算的數字爲正數或爲負數,在執運算時,都會在高位補0。數組
左移 對於左移是沒有正數跟負數這一說的,由於負數在CPU中是以補碼的形式存儲的,對於正數左移至關於乘以2的N次冪。安全
敲重點:上面的重重都是簡單的只是爲了引出下面的結論:數據結構
a % (Math.pow(2,n))
等價於a&( Math.pow(2,n)-1)
多線程
好比a%16
最終的結果必定是0~15
之間的數字,而a&1111
正好把a除16後的餘數有效的現實出來了由於若是是1 1111這樣的話最前面一位其實表明的16,也就是說二進制從倒數第五位開始只要出現了1那絕對錶明的是16的倍數。 結論:位運算比除法運算在運行效率上更高,對一個數取餘儘可能用a&二進制數
這樣能夠更好的提速。
咱們知道ArrayList
是一個數組隊列,至關於動態數組。與Java中的基本數組相比,它的容量能動態增加。它具備如下幾個重點。
ArrayList 其實是經過一個 數組去保存數據的。當咱們構造ArrayList時;若使用默認構造函數,則ArrayList的默認容量大小是 10。 當ArrayList容量不足以容納所有元素時,ArrayList會從新設置容量:原來容量的1.5倍。 ArrayList的克隆函數,便是將所有元素 克隆到一個數組中。 克隆的底層是System.arraycopy(0,oldsrc,0,newsrc,length); ArrayList實現java.io.Serializable的方式。當寫入到輸出流時,先寫入「容量」,再依次寫入「每個元素」;當讀出輸入流時,先讀取「容量」,再依次讀取「每個元素」。
優勢:
根據下標遍歷元素效率較高。 根據下標訪問元素效率較高。 在數組的基礎上封裝了對元素操做的方法。 這樣的動態數組在內地地址上是空間連續的。 能夠自動擴容。
缺點:
插入和刪除的效率比較低。 根據內容查找元素的效率較低。 擴容規則:每次擴容現有容量的50%。
雙向鏈表每個節點包含三部分(data,prev,next),它不要求空間是連續的。相似於節點跟節點之間經過先後兩條線串聯起來的。
ArrayList和LinkedList總結:
ArrayList是實現了基於動態數組的數據結構,LinkedList是基於鏈表結構。 對於隨機訪問的get和set方法,ArrayList要優於LinkedList,由於LinkedList要移動指針。 對於新增和刪除操做add和remove,LinkedList比較佔優點,由於ArrayList要移動數據。 ArrayList使用在 查詢比較多,可是插入和刪除比較少的狀況,而LinkedList用在查詢比較少而插入刪除比較多的狀況
首先你須要對二叉樹有個瞭解,知道這是什麼樣子的一個數據組合方式,而後知道二叉樹查找的時候缺點,可能發生數據傾斜。所以引入了平衡二叉樹,平衡二叉樹的左右節點深度之差不會超過1,查找方便構建麻煩,所以又出現了紅黑樹。紅黑樹是一種平衡的二叉查找樹,是一種計算機科學中經常使用的數據結構,最典型的應用是實現數據的關聯,例如map等數據結構的實現,紅黑樹重要特性是( 左節點 < 根節點 < 右節點) 紅黑樹有如下限制:
節點必須是紅色或者是黑色 根節點是黑色的 全部的葉子節點是黑色的。 每一個紅色節點的兩個子節點是黑色的,也就是不能存在父子兩個節點全是紅色 從任意每一個節點到其每一個葉子節點的全部簡單路徑上黑色節點的數量是相同的。
若是您對紅黑樹還不太瞭解推薦看下博主之前寫的RBT
Hash表是一種特殊的數據結構,它同數組、鏈表以及二叉排序樹等相比較有很明顯的區別,它可以快速定位到想要查找的記錄,而不是與表中存在的記錄的關鍵字進行比較來進行查找。這個源於Hash表設計的特殊性,它採用了==函數映射==的思想將記錄的存儲位置與記錄的關鍵字關聯起來,從而可以很快速地進行查找。評價函數的性能關鍵在於==裝填因子==,以及如何合理的解決哈希衝突,具體的可看博主之前寫的完全搞定哈希表
一般具有前面一些知識點的鋪墊就能夠很好的開展HashMap的講解了,既然ArrayList
,LinkedList
,Red Black Tree
各有優缺點,咱們能不能集百家之長實現一個綜合產物呢 === >HashMap
,本文因此講解都是基於JDK8。
HashMap
的組成部分:數組 + 鏈表 + 紅黑樹。HashMap
的主幹是一個Node
數組。Node
是HashMap
的基本組成單元,每個Node
包含一個key-value
鍵值對。HashMap
的時間複雜讀幾乎能夠接近O(1)
(若是出現了 哈希衝突可能會波動下),而且HashMap
的空間利用率通常就是在40%左右。HashMap
的大體圖以下: PS:其中幾個重要節點關係以下:
interface
定義了一些比較的接口函數。
HashMap
中存儲的基本的KV。
Enrty
這個類繼承自
HashMap.Node
這個類,
Enrty
是
LIinkedHashMap
的一個內部類,
TreeNode
的構造函數向上追溯繼承了
LinkedHashMap.Entry
,然後者又繼承了
HashMap.Node
。因此
TreeNode
既保有
Node
的屬性,同時因爲添加了
prev
這個前驅指針使得==鏈表==變爲了==雙向==。前三個節點跟第五個紅黑樹相關,第四個跟
next
跟雙向鏈表相關。
HashTable
裏的映射函數來決定將該數據放到數組的那個地方,數組初始化時候必定是2的次冪,默認16,初始化傳入的任何數字都會通過
tableSizeFor
調整爲2次冪。
TREEIFY_THRESHOLD=8
且數組長度 >=
MIN_TREEIFY_CAPACITY=64
,則會將該鏈表進化位
RedBlackTree
,若是
RedBlackTree
中節點個數小於
UNTREEIFY_THRESHOLD=6
會退化爲鏈表。
特別提醒:讀HashMap源碼以前須要知道它大體特性以下:
HashMap的存取是沒有順序的 KV均容許爲NULL 多線程狀況下該類安全,能夠考慮用HashTable。 JDk8底層是數組 + 鏈表 + 紅黑樹,JDK7底層是數組 + 鏈表。 初始容量和裝載因子是決定整個類性能的關鍵點,輕易不要動。 HashMap是 懶漢式建立的,只有在你put數據時候纔會build 單向鏈表轉換爲紅黑樹的時候會先變化爲 雙向鏈表最終轉換爲 紅黑樹,雙向鏈表跟紅黑樹是 共存
的,切記。對於傳入的兩個 key
,會強制性的判別出個高低,判別高低主要是爲了決定向左仍是向右。鏈表轉紅黑樹後會努力將紅黑樹的 root
節點和鏈表的頭節點 跟table[i]
節點融合成一個。在刪除的時候是先判斷刪除節點紅黑樹個數是否須要轉鏈表,不轉鏈表就跟 RBT
相似,找個合適的節點來填充已刪除的節點。紅黑樹的 root
節點不必定
跟table[i]
也就是鏈表的頭節點是同一個哦,三者同步是靠MoveRootToFront
實現的。而HashIterator.remove()
會在調用removeNode
的時候movable=false
。
初始容量,默認容量=16,箱子的個數不能太多或太少。若是太少,很容易觸發擴容,若是太多,遍歷哈希表會比較慢。
數組的最大容量,通常狀況下只要內存夠用,哈希表不會出現問題
默認的負載因子。所以初始狀況下,當存儲的全部節點數 > (16 * 0.75 = 12 )時,就會觸發擴容。 默認負載因子(0.75)在時間和空間成本上提供了很好的折衷。較高的值會下降空間開銷,但提升查找成本(體如今大多數的HashMap類的操做,包括get和put)。設置初始大小時,應該考慮預計的entry數在map及其負載係數,而且儘可能減小rehash操做的次數。若是初始容量大於最大條目數除以負載因子,rehash操做將不會發生。
從上面的表中能夠看到當桶中元素到達8個的時候,機率已經變得很是小,也就是說用0.75做爲加載因子,每一個碰撞位置的鏈表長度超過8個是幾乎不可能的。 4. static final int TREEIFY_THRESHOLD = 8
這個值表示當某個箱子(數組的某個item)中,鏈表長度 >= 8 時,有可能會轉化成樹。設置爲8,是系統根據泊松分佈的數據分佈圖來設定的。
在哈希表擴容時,若是發現鏈表長度 <= 6,則會由樹從新退化爲鏈表。 設置爲6猜想是由於時間和空間的權衡
當鏈表長度爲6時 查詢的平均長度爲 n/2=3,紅黑樹 log(6) = 2.6 爲8時 :鏈表 8/2=4, 紅黑樹 log(8)=3
鏈表轉變成樹以前,還會有一次判斷,只有數組長度大於 64 纔會發生轉換。這是爲了不在哈希表創建初期,多個鍵值對剛好被放入了同一個鏈表中而致使沒必要要的轉化。
HashMap的鏈表數組。不管咱們初始化時候是否傳參,它在自擴容時老是2的次冪。
HashMap實例中的Entry的Set集合
HashMap表中存儲的實例KV個數。
凡是咱們作的增刪改都會引起
modCount
值的變化,跟版本控制功能相似,能夠理解成version
,在特定的操做下須要對version
進行檢查,適用於Fai-Fast
機制。在java的集合類中存在一種
Fail-Fast
的錯誤檢測機制,當多個線程對同一集合的內容進行操做時,可能就會產生此類異常。 好比當A經過iterator去遍歷某集合的過程當中,其餘線程修改了此集合,此時會拋出ConcurrentModificationException
異常。 此類機制就是經過modCount
實現的,在迭代器初始化時,會賦值expectedModCount
,在迭代過程當中判斷modCount
和expectedModCount
是否一致。
擴容閾值 threshold = capacity * loadFactor
可自定義的負載因子,不過通常都是用系統自帶的0.75。
先來分析有關n位操做部分:先來假設n的二進制爲01xxx...xxx。接着 對n右移1位:001xx...xxx,再位或:011xx...xxx 對n右移2爲:00011...xxx,再位或:01111...xxx 此時前面已經有四個1了,再右移4位且位或可得8個1 同理,有8個1,右移8位確定會讓後八位也爲1。
綜上可得,該算法讓最高位的1後面的位全變爲1。最後再讓結果n+1,即獲得了2的整數次冪的值了。 如今回來看看第一條語句:
int n = cap - 1;
讓cap-1再賦值給n的目的是另找到的目標值大於或等於原值。例如二進制1000,十進制數值爲8。若是不對它減1而直接操做,將獲得答案10000,即16。顯然不是結果。減1後二進制爲111,再進行操做則會獲得原來的數值1000,這種二進制方法的效率很是高。
map
的大小來反推須要的
threshold
,同時還可能會涉到
resize
,而後住個
put
到 容器中。
不管咱們put
數據仍是get
數據都要先得到該數據在這個哈希表中對應的位置。好比put
數據,它的流程分爲2步。
1.先得到key對應的hash值。 2. 將該數據的hash值A,跟將A右無符號移動16位後再
^
獲得最終值。這個操做叫擾動
,緣由是怕低幾位出現想同的機率太大,儘量的將數據實現均勻分佈。
同時JDK8跟JDK7的擾動目的同樣,不過複雜程度不同。
相對來講很簡單,爲方便理解先說下代碼大體流程思路。
得到key的hash而後根據hash和key按照插入時候的思路去查找 get
。若是數組位置爲NULL則直接返回 NULL。 若是數組位置不爲NULL則先直接看數組位置是否符合。 若是數組位置有類型說紅黑樹類型,則按照紅黑樹類型查找返回。 若是數組有next,則按照遍歷鏈表的方式查找返回。
宏觀查找函數細節:
紅黑樹查找節點細節:
先得到根節點,左節點,右節點。 根據 左節點 < 根節點 < 右節點 對對數據進行逐步範圍的縮小查找。 若是實現了Comparable方法則直接對比。 不然若是根節點不符合則遞歸性的調用find查找函數。
查詢該key是否實現了Comparable
接口。
既然實現了Comparable接口就用該實現進行對比判斷如何何去何從。
跟隨源碼梳理下put操做的大體流程。 數據插入的時候大體流程以下:
Hash
值計算。
table
的狀態,若是
table
是空須要調用
resize
來進行初始化。
key
的目標位置。並判斷當前位置狀況。
putTreeVal
。
在JDK8中尋找待插入點
e
是經過==尾插法==(相似與排隊在最後面),而在JDK7中是==前插法==(相似與加塞在最前面,之因此這樣作是HashMap發明者認爲後插入節被訪問機率更大),對應代碼以下。
e
進行判斷
oldValue對應的舊值若是爲NULL,那麼不管onlyIfAbsent是否決定替換。都將被替換。 oldValue對應的舊值若是不爲NULL,那麼若是onlyIfAbsent是false就替換。 onlyIfAbsent:只有在缺席的狀況下才替換,不缺席不替換。跟redis Setnx
一樣的功能。
modCount
加1,同時看最新的總的節點數是否須要擴容了,若是是就擴容。
主要功能是根據參數的閾值範圍絕對是否將鏈表轉化爲紅黑樹,而後首先將單項鍊表轉化爲雙向鏈表,再調用treeify
以頭節點爲根節點構建紅黑樹。
雙向鏈表跟紅黑樹建立,主要步驟分3步。
table[i]
對應好。
確保將root
節點挪動到table[first]
上,若是紅黑樹構建成功而沒成功執行這個任務會致使tablle[first]
對應的節點不是紅黑樹的root
節點。正常執行的時候主要步驟分2步。
root
節點放到跟節點,至此關於紅黑樹到操做搞定。
first
節點,如今將多是中間節點的
root
節點挪到
first
節點前面。
checkInvariants
函數的做用:校驗
TreeNode
對象是否知足紅黑樹和雙鏈表的特性。由於併發狀況下會發生不少異常。
得到老table數據,若是老table已經足夠大則再也不擴容,只調節閾值。 老table擴容後的範圍也 符合要求直接將容器大小跟閾值都擴容 。 若是是帶參數構造函數則須要將閾值複製給容器容量。 不然認爲該容器初始化時未傳參,需初始化。 若是老table有數據,新他變了大小設置好了可是閾值沒設置成功。此時要設置新閾值。 建立新容器。 老table成功擴容爲新table,涉及到數據的轉移。
數據不爲空是單獨的節點則直接從新hash分配新位置。 數據不爲空後面是一個鏈表,則要把鏈表數據進行區分看那些分到老地方那些分到新地方。 若是該節點類型是個紅黑樹則調用split.
鏈表形式的從新劃分解釋以下: 注意:不是
(e.hash & (oldCap-1))
而是(e.hash & oldCap)
, 後一個獲得的是 元素的在數組中的位置是否須要移動,示例以下
示例1: e.hash= 10 0000 1010 oldCap=16 0001 0000 & =0 0000 0000 比較高位的第一位 0 結論:元素位置在擴容後數組中的位置沒有發生改變 示例2: e.hash= 17 0001 0001 oldCap=16 0001 0000 & =1 0001 0000 比較高位的第一位 1 結論:元素位置在擴容後數組中的位置發生了改變,新的下標位置是原下標位置+原數組長度
擴容後如何處理原來一個table[i]
上的紅黑樹,代碼的總體思路跟處理鏈表的時候差很少,只要理解節點關係保存紅黑樹的時候也保存了雙向鏈表就OK了。
函數功能就是以指定的一個節點爲根節點,根據指定的key
跟value
進行查找。
經過hash值判斷 左邊找仍是右邊找。 若是找到的很簡單直接返回。 可能出現hash值相等但是 key
不同,繼續查找分爲三種狀況。
左節點爲空則找右節點 右節點爲空則找左節點 左右節點都不會空,嘗試通 Comparable
對數據看向左仍是向右。沒法經過comparable比較或者比較以後仍是相等。
直接從右節點遞歸查找下。 不然就從左邊查找。
對兩個對象進行比較,必定能比出個高低。
a 跟 b 都是字符串則直接在if判斷裏比拼完畢 a 跟 b 都是對象則直接查看對象在JVM中的hash地址,而後比較。
函數入口而已:
removeNode
無非就是查看table[i]是否存在,而後是否在首節點上,是否在紅黑樹上,是否在鏈表上。這幾種狀況,找到了則直接刪除,同時注意平衡性。
該函數的 目的就是移除調用此方法的節點,也就是該方法中的this節點。移除包括鏈表的處理和紅黑樹的處理。能夠看之前寫過的RBT,刪除的時候思路大體是同樣的,這裏大體分爲3步驟。
紅黑樹也是雙向鏈表,以鏈表的角度來刪除節點,而後判斷是否須要退化爲鏈表。 根據當前的 p
節點嘗試從pr
找最 小的或者從pl
找最 大的目標節點s
,將兩點兌換。找到要 replacement
來跟p
進行替換。實施替換。 替換後爲保持紅黑樹特性可能須要進行 balance
。
紅黑樹退化成鏈表
關於這個問題能夠直接看博主之前寫的紅黑叔添加跟刪除RBT
JDK7對舊table
數據重定位到新table
的函數transfer
以下,其中重點關注部分以標出。
Entry<K,V> next = e.next
就被掛起了,而線程2正常執行完畢,結果圖以下:
7中找 Hash
用了4次,8中只用了1次。7 = 數組 + 鏈表,8 = 數組 + 鏈表 + 紅黑樹 7中是頭插法,多線程容易形成環,8中是尾插法。 7的擴容是所有數據從新定位,8中是位置不變+ 移動舊size大小來實現更好些。 7是先判斷是否要擴容再插入,8中是先插入再看是否要擴容。 HashMap
無論78都是現場不安全的,多線程狀況下記得用ConcurrentHashmap
。ConcurrentHashmap
下篇文章說。
隨機蒐羅了一些常見HashMap
問題,若是把上述代碼都看懂了應付這些應該沒問題。
HashMap原理,內部數據結構。 HashMap中的put,get,remove大體過程。 HashMap中 hash函數實現。 HashMap如何擴容。 HashMap幾個重要參數爲何這樣設定。 HashMap爲何線程不安全,如何替換。 HashMap在JDK7跟JDK8中的區別。 HashMap中鏈表跟紅黑樹切換思路。
HashMap講解 HashMap詳解 疫苗JAVA HASHMAP的死循環
本文使用 mdnice 排版