開局一張圖java
Java8對Java7的HashMap作了修改,最大的區別就是利用了紅黑樹。算法
Java7的結構中,查找數據的時候,咱們會根據hash值快速定位到數組的具體下標。可是後面是須要經過鏈表去遍歷數據,因此查詢的速度就依賴於鏈表的長度,時間複雜度也天然是O(n)數組
爲了減小2中出現的問題,在Java8中,當鏈表的個數大於8的時候,就會把鏈表轉化爲紅黑樹。那麼在紅黑樹查找數據的時候,時間複雜度就變味了O(logN)安全
結構圖函數
描述oop
數組中存放的是節點。post
若是是鏈表,就是Node節點,紅黑樹的話則是TreeNode節點。this
在Node節點中,都是keyt,value,hash,next這幾個屬性,和Java7的基本同樣。spa
咱們根據存放在數組中的節點的類型,判斷是紅黑樹仍是鏈表.net
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);
}
複製代碼
這個構造方法初始化了閾值和負載因子。
在構造方法中,是不會指定HashMap的容量大小的,就算是用HashMap(int initCapacity)
的構造方法,傳入的數運算以後的結果後面只是初始化閾值,並無立刻構建內部的數組,至於初始化內部數組只有第一次put的時候纔會執行,初始化閾值是用來方便後面put的時候初始化數組。具體的還得須要讀者往下看,只是說明下內部數組並非在構造函數執行就已經初始化了。
這個函數就是將傳進來的數向上取到最接近2的冪次的數(包括等於)。好比傳入15,返回則是16;傳入18,返回32。傳入32,返回32。咱們來看看他如何實現吧!
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;
}
複製代碼
在解釋源碼以前我先說一下,傳入一個cap,可能它不是2的幾回冪,要找到大於等於cap的最小的2的冪
怎麼找呢?咱們看看開始舉的例子(這裏先把18-1先,至於這個-1後面會講)
00000000,00000000, 00000000, 0001 0001 十進制:17
00000000,00000000, 00000000, 0010 0000 十進制:32
再看32-1的二進制是多少,和32和17的對比一下
00000000,00000000, 00000000, 0010 0000 十進制:32
00000000,00000000, 00000000, 0001 1111 十進制:31
00000000,00000000, 00000000, 0001 0001 十進制:17
咱們看到只要將17的後面5位所有變爲1,那麼就成31的二進制,後面再+1就能夠變爲32,這就達到咱們的目的了!一句話說,這個函數的目的就是從最左邊的1開始往右,都要變爲1,後面再+1就能夠達到咱們想要的目的了!
n|=n>>>1
因爲n大於0,那麼在二進制中高位確定有一位是1,那麼無符號右移1位與本身相或,那麼確定是接近原來數的後面的數變爲1.好比10xxxx,那麼運算 n|= n>>>1以後,確定是11xxxx。
n|=n>>>2
在上面的例子中,n已經由10xxxx變爲11xxxx了。那麼咱們們要讓後面xxxx繼續變爲1,此時有兩位是1,那麼就讓xxxx的前兩位繼續和11相或唄。因此無符號右移兩位再與本身相或,就能夠從11xxxx變爲1111xx了。
那麼如今有4個1,那麼後面就移4位。變爲8個1,就移8位....
依次類推。
若是要把32位變爲全1的話,只要先把前16位變爲1,那麼後面右移16位就能夠把32位所有變爲1啦。
咱們也能夠看到,32位的1確定是超過MAXIMUM_CAPACITY(1<<30),那麼後面結果就會變爲MAXIMUM_CAPACITY啦。
給你們看個圖把,或許會更加清楚(用的是別人圖)。其實上面說得很清楚啦!
這裏說下爲何傳進來要先減1。有前面咱們都知道,
相信上面的解釋和例子中你都應該理解吧!二進制最左邊的1開始往右,都要變爲1。若是傳進來的恰好是2的m次冪,那麼後面的n的二進制會變爲1+上m個1,那返回的時候再+1的話,就變爲1加上m+1個0了。那麼就變爲傳進來的數字的兩倍了。好比說傳進32,二進制爲100000,1後面5個0。後面是n變爲111111,返回的時候+1,那麼就返回1000000,對應的十進制就是64了。這顯然不符咱們的邏輯。
至於其它不是2的幾回冪的數,無論減不減1,只要最左邊的那個1位置沒變,右邊不論是什麼,到後面都是能夠變爲大於等於傳進來的數的最小的2的冪的。
因此綜上,傳進來的cap-1,就是爲了防止傳進來的cap是恰好的2的冪次數,避免後面返回的時候翻倍。
public V put(K key, V value) {
//關於hash函數,後面會說
return putVal(hash(key), key, value, false, true);
}
//---------putVal
//onlyIfAbsent若是爲true的表示的是:若是key不存在就存入,存在就不存入
//爲false:key存在也存入,只不過會覆蓋舊值,而後把舊值返回
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//第一次put,會執行下面的if裏面的 resize()
//第一次resize就是至關於初始化, 通常都會設置爲16,後面擴容就不同了。
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//假設是第一次擴容,(n-1) & hash 至關於 hash對 n 求模
//這裏也就是將hash值對15求模就能夠隨機獲得一個下標啦
//若是這個位置沒有值,那麼就直接初始化一下Node而且放在hash映射到的位置
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//這裏hash映射的數據已經有節點啦
else {
Node<K,V> e; K k;
//若是第一個節點就是咱們想要找的那個節點,那麼e就執行這第一個節點
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);
//到這裏,就是想要放入的key可能在第一個節點後邊或者這個key在鏈表中也不存在
else {
//這裏binCount進行計數,主要是爲了記錄是否到達8個節點從而進行變形爲紅黑樹
for (int binCount = 0; ; ++binCount) {
//若是到達最後一個節點,也就是這個key不存在的狀況
if ((e = p.next) == null) {
//新鍵節點放在鏈表的尾節點的後面,此時e已經爲null
p.next = newNode(hash, key, value, null);
//若是此時節點已經到達7個,那麼加入這個節點就成爲8個了,那麼就進行轉化爲紅黑樹啦
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
//這裏有兩種狀況 1 是成功加入鏈表尾部,而且總數沒超過8 ,2是 加入節點以後,總數到達8,那麼就轉爲紅黑樹,就退出循環了
break;
}
//put的時候,若是key已經存在在鏈表中,那麼就退出,後面再進行 覆蓋 操做
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
//這裏是一直沒找到,遍歷鏈表的操做
p = e;
}
}
//這裏e不爲null的狀況就是put的key已經在以前的鏈表中,
//爲null的話就是不在以前的鏈表中而且已經加入到以前鏈表的尾部
if (e != null) {
V oldValue = e.value;
//1 這個onlyIfAbsent以前也說過,爲false就能夠 覆蓋 舊值
//2 或者以前就沒有值
//1 或者 2 就執行下面的if
if (!onlyIfAbsent || oldValue == null)
e.value = value;
//這個函數只在LinkedHashMap中用到, 這裏是空函數
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//若是增長這個節點以後,超過了閾值,那麼就進行擴容
if (++size > threshold)
resize();
//這個函數只在LinkedHashMap中用到, 這裏是空函數
afterNodeInsertion(evict);
return null;
}
複製代碼
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
複製代碼
在java中, hash函數是一個native方法, 這個定義在Object類中, 因此全部的對象都會繼承.
public native int hashCode();
複製代碼
由於這是一個本地方法, 因此沒法肯定它的具體實現, 可是從函數簽名上能夠看出, 該方法將任意對象映射成一個整型值.調用該方法, 咱們就完成了 Object -> int
的映射
在hash的實現中咱們看到
若是key爲null,那麼這個值就是放在數組的第一個位置的。
若是key不爲null,那麼就會先去key的hashCode右移16位而後再與本身異或。
你們可能關於第2點有點疑問
其實也就是說,經過讓hashcode的高16位和低16位異或,經過高位對低位進行了干擾。目的就是爲了讓hashcode映射的數組下標更加平均。下面這段是引用論壇的匿名用戶的解釋,我的以爲解釋得很詳細
做者:匿名用戶
來源:知乎
咱們建立一個hashmap,其entry數組爲默認大小16。 如今有一個key、value的pair須要存儲到hashmap裏,該key的hashcode是0ABC0000(8個16進制數,共32位),若是不通過hash函數處理這個hashcode,這個pair過會兒將會被存放在entry數組中下標爲0處。下標=ABCD0000 & (16-1) = 0。 而後咱們又要存儲另一個pair,其key的hashcode是0DEF0000,獲得數組下標依然是0。 想必你已經看出來了,這是個實現得不好的hash算法,由於hashcode的1位全集中在前16位了,致使算出來的數組下標一直是0。因而,明明key相差很大的pair,卻存放在了同一個鏈表裏,致使之後查詢起來比較慢。 hash函數的經過若干次的移位、異或操做,把hashcode的「1位」變得「鬆散」,好比,通過hash函數處理後,0ABC0000變爲A02188B,0DEF0000變爲D2AFC70,他們的數組下標再也不是清一色的0了。 hash函數具體的作法很簡單,你畫個圖就知道了,無非是讓各數位上的值受到其餘數位的值的影響。
在源碼中咱們看到h&(n-1)
的操做,其實這樣是和 h % n
同樣的。只不過是一個很大的求模的時候會影響效率,可是經過位運算就快不少啦!
resize()用於HashMap的初始化數組和數組擴容.
數組擴容以後,容量都是以前的2倍
進行數據遷移
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
//若是是初始化,oldTab確定null,反之就不是null
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
}
//這裏調用new HashMap(initCapacity),第一次put
else if (oldThr > 0)
//指定了容量,好比initCapacity指定了22,那麼newCap就是32
newCap = oldThr;
//這裏調用new HashMap(),第一次put
else {
//容量就是默認類內部指定的容量,也就是16
newCap = DEFAULT_INITIAL_CAPACITY;
//默認的加載因子是0.75,因此閾值就是16*0.75 = 12
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//這裏的狀況是 調用了new HashMap(initCapacity)或者
//new HashMap(initCapacity,loadFactor)的狀況
//由於上面的兩個構造函數都會初始化 loadFactor
//就是根據新的容量初始化新的閾值
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
//建立新的數組,賦給table,也就是實現了擴容
@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;
//獲取對應數組位置的節點,若是爲null表示沒節點
//不然就轉移
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 {
//這裏定義了兩個鏈表,lo和hi
//此時 e 就是數組所對應的第一個節點
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
//下面這個do-while就是遍歷數組當前位置的鏈表,而後
//根據某些規則,把鏈表的節點放在擴容後的數組的不一樣位置
do {
//獲取e的下一個節點
next = e.next;
//下面的解釋可能有點難懂,待會看下面的解釋再看這裏便可
//1 若是節點hash運算後是老位置,那麼就用lo鏈表存儲
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
//2 那麼就是新位置,就用hi鏈表存儲
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
//這裏很簡單,lo鏈表不爲空,那麼數組的老位置就放lo的頭結點
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
//hi鏈表不爲空,那麼數組的老位置就放hi的頭結點
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
複製代碼
下面用一張圖解釋下數據轉移的過程。
如今來解釋下resize源碼中的疑問
你們可能對(e.hash & oldCap) == 0
這個判斷有點迷糊,下面就來解釋下。
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
由put方法的這兩個語句咱們知道,這裏是經過hash和數組的長度-1相與獲得節點映射到數組的哪一個位置的。
一:
其實很簡單,n就是數組的長度,咱們都知道,在HashMap中的數組長度確定是2的m方。那麼n-1在二進制中就是m個全1咯,好比說數組的長度是16,16就是2的4次方,那麼16-1 = 15 就是4個全1(2進制) 「1111」
沒擴容前就是用hash(key)以後獲得的hash值 與 (n-1)相與 獲得位置,在上面的例子中也就是取出hash值的低4位,結果爲a。(結果確定在0-15之間)
擴容後,數組變爲以前的2倍,那麼數組的長度就成爲32了,二進制就是100000,那麼照葫蘆畫瓢,同一個 hash值與(32-1)相與獲得位置。也就是 取出hash值的低5位,結果爲b。(結果確定在0-31之間)
二:
由1和2得知,b的二進制比a的二進制多了1位,前面4位是相同的。而且在二進制中,不是0就是1。
因此咱們得出結論,只要咱們能夠判斷擴容以後b比a多的那一位是1仍是0(在例子中也就是第5位),就能夠得出同一個節點在新數組的哪一個位置了,一句話總結就是。數組擴容後,同一個節點要麼在原來的位置,要麼在原來的位置加上沒擴容的數組的長度(oldCap)。
那麼咱們如何獲得b比a多的那一位究竟是啥呢?很簡單,就是用hash值和oldCap相與便可!好比說這裏的oldCap爲16,那麼二進制就是1後面加上m個0,也就是10000,也就是第m+1位爲1,用hash值與oldCap相與,就能夠得出hash值的第5位是啥啦,那麼就能夠根據前面的"二"去判斷節點到底在新數組哪一個位置啦。
讀者們看到這裏,就能夠繼續回到源碼的1處去看,這時候應該就會豁然開朗啦!
擴容時,會將原table中的節點re-hash到新的table中, 但節點在新舊table中的位置存在必定聯繫: 要麼下標相同, 要麼相差一個oldCap
(原table的大小).
CocurrentHashMap
或者Collections.synchronizedMap
將HashMap對象封裝成線程安全的。