特色:java
底層數據結構:node
注意: 鏈表轉化爲紅黑樹以前會進行判斷,若果閾值大於8,可是數組長度小於64,這時鏈表不會轉化爲紅黑樹去存儲數據,而是會對數組進行擴容。面試
這樣作的緣由: 若是數組比較小,應儘可能避免紅黑樹結構。由於紅黑樹結構較爲複雜,紅黑樹又稱爲平衡二叉樹,須要進行左旋、右旋、變色這些操做才能保證平衡。在數組容量較小的狀況下,操做數組要比操做紅黑樹更節省時間。綜上所述:爲了提升性能以及減小搜索時間,在閾值大於8而且數組長度大於64的狀況下鏈表纔會轉化爲紅黑樹而存在。具體參考treeifyBin
方法。算法
HashMap存儲數據結構圖:數組
package hashmap_demo;
import java.util.HashMap;
public class HashMapTest {
public static void main(String[] args) {
HashMap<String, Integer> map = new HashMap<>();
map.put("柳巖", 18);
map.put("楊冪", 28);
map.put("劉德華", 40);
map.put("柳巖", 20);
System.out.println(map);
}
}
//輸出結果:{楊冪=28, 柳巖=20, 劉德華=40}
複製代碼
1.當執行HashMap<String, Integer> map = new HashMap<>();
這行代碼建立HashMap實例對象時;在JDK1.8以前,會在構造方法中建立一個長度爲16 的Entry[] table數組用來存儲鍵值對;JDK1.8以後,建立數組的時機發生了變化,不是在構造方法中建立數組了,而是在第一次調用put()
方法時(即第一次向HashMap中添加元素)建立Node[] table數組。安全
注意: 建立HashMap實例對象在JDK1.8先後發生了變化,主要有兩點:建立的時機發生了變化;數組類型發生了變化,由原來的Entry[]
類型變爲Node[]
類型。bash
2.向哈希表中存儲柳巖-18
,會根據柳巖
調用String
類中重寫後的hashCode()
方法計算出柳巖
對應的哈希值,而後結合數組長度採用某種算法計算出柳巖
在Node[]數組中的索引值。若是該索引位置上無數據,則直接將柳巖-18
插入到該索引位置。好比計算出柳巖
對應的索引爲3,如上圖所示。數據結構
面試題:哈希表底層採用那種算法計算出索引值?還有哪些算法計算索引值?less
答:採用key的hashCode()方法計算出哈希值,而後結合數組長度進行無符號右移(>>>)、按位異或(^)、按位與(&)計算出索引值;還能夠採用平方取中法、取餘數、僞隨機數法。dom
取餘數:10%8=2 11%8=3;位運算效率最高,其餘方式效率較低。
3.向哈希表中存儲楊冪-28
,計算出該索引位置無數據,直接插入。
4.向哈希表中存儲劉德華-40
,假設劉德華
計算出的索引也是3,那麼此時該索引位置不爲null,這時底層會比較柳巖
和劉德華
的哈希值是否一致,若是不一致,則在此索引位置上劃出一個節點來存儲劉德華-40
,這種方式稱爲拉鍊法。
補充:索引計算源碼p = tab[i = (n - 1) & hash]
,即索引=哈希值&(數組長度-1),按位與運算等價於取餘運算,由於19%4=3,19%8=3,因此會出現同一個數組,索引值相同,但哈希值不一樣的狀況。
5.最後向哈希表中存儲柳巖-20
,柳巖
對應的索引值爲3。由於該索引位置已有數據,因此此時會比較柳巖
與該索引位置上的其餘數據的哈希值是否相等,若是相等,則發生哈希碰撞。此時底層會調用柳巖
所屬String
字符串類中的equals()
方法比較兩個對象的內容是否相同:
相同:則後添加數據的value值會覆蓋以前的value值,即柳巖-20
覆蓋掉柳巖-18
。
不相同:繼續和該索引位置的其餘對象進行比較,若是都不相同,則向下劃出一個節點存儲(拉鍊法)。
注意點:若是一個索引位置向下拉鍊,即鏈表長度大於閾值8且數組長度大於64,則會將此鏈表轉化爲紅黑樹。由於鏈表的時間複雜度爲O(N),紅黑樹的時間複雜度爲O(logN),鏈表長度多大時O(N)>O(logN)。
首先看添加元素的put()
方法流程:
說明:
size
表示HashMap
中K-V
的實時數量,不等於數組的長度;threshold
(臨界值)=capacity
(數組容量)*loadFactory
(加載因子),臨界值表示當前已佔用數組的最大值。size
若是超過這個臨界值進調用resize()
方法進行擴容,擴容後的容量是原來的兩倍;16*0.75=12
,即HashMap
中存儲的元素超過12
就會進行擴容。是原來容量的2倍,即HashMap是以2n進行擴容的。
複製代碼
HashMap的無參構造,默認初始值爲16,源碼以下:
/** * Constructs an empty <tt>HashMap</tt> with the default initial capacity * (16) and the default load factor (0.75). */
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
複製代碼
默認初始值源碼:
/** * The default initial capacity - MUST be a power of two. * 默認初始容量必須是2的冪 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
複製代碼
由源碼能夠看到,HashMap
的默認初始容量爲1左移4位,即1*2的4次方爲16。若是使用HashMap的無參構造進行初始化,第一次put
元素時,會觸發resize()
方法(擴容方法),擴容後的容量爲16。這一點和ArrayList
初始化過程很類似(使用ArrayList
的無參構造初始化時,建立的是一個空數組,當第一次向空數組添加元素時會觸發grow()
擴容方法,擴容後的容量爲10)。
HashMap的有參構造,便可以指定初始化容量大小,源碼以下:
/** * Constructs an empty <tt>HashMap</tt> with the specified initial * capacity and the default load factor (0.75). * * @param initialCapacity the initial capacity. * @throws IllegalArgumentException if the initial capacity is negative. */
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
複製代碼
即構造一個指定容量和默認加載因子(0.75)的空HashMap
。
由上面的內容咱們知道,當向HashMap
中添加元素時,首先會根據key
的哈希值結合數組長度計算出索引位置。HashMap
爲了存取高效須要減小哈希碰撞,使數據分配均勻,採用按位與**hash&(length-1)**計算索引值。
HashMap
採用取餘的算法計算索引,即hash%length
,可是取餘運算不如位運算效率高,因此底層採用按位與**hash&(length-1)**進行運算。兩種算法等價的前提就是length
是2的n次冪。
咱們須要知道兩個結論:
舉例說明爲何數組長度是2的n次冪能夠均勻分佈:
按位與運算:相同二進制位上都是1,結果爲1,不然爲0。
假設數組長度爲2的3次冪8,哈希值爲3,即3&(8-1)=3,索引爲3;
假設數組長度爲2的3次冪8,哈希值爲2,即2&(8-1)=2,索引爲2;
運算過程以下:
3&(8-1)
0000 0011 -->3
0000 0111 -->7
----------------
0000 0011 -->3
2&(8-1)
0000 0010 -->2
0000 0111 -->7
----------------
0000 0010 -->2
結論:索引值不一樣,不一樣索引位置都有數據分佈,分佈均勻。
複製代碼
假設數組長度不是2的n次冪,好比長度爲9,運算過程以下:
假設數組長度爲9,哈希值爲3,即3&(9-1)=3,索引爲0;
假設數組長度爲9,哈希值爲2,即2&(9-1)=2,索引爲2;
運算過程以下:
3&(9-1)
0000 0011 -->3
0000 1000 -->8
----------------
0000 0000 -->0
2&(9-1)
0000 0010 -->2
0000 1000 -->8
----------------
0000 0000 -->0
結論:索引值都爲0,致使同一索引位置上有不少數據,而其餘索引位置沒有數據,導致鏈表或紅黑樹過長,效率下降。
複製代碼
注意: hash%length
等價於hash&(length-1)
的前提條件是數組長度爲2的n次冪。因爲底層採用按位與運算計算索引值,因此須要保證數組長度必須爲2的n次冪。
這時HashMap會經過位運算和或運算獲得一個2的冪次方數,而且這個數是離指定容量最小的2的冪次數。好比初始容量爲10,通過運算最後會獲得16。
複製代碼
該過程涉及到的源碼以下:
//建立HashMap集合對象,並指定容量爲10,不是2的冪
HashMap<String, Integer> map = new HashMap<>(10);
//調用有參構造
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//this關鍵字繼續調用
public HashMap(int initialCapacity, float loadFactor) {//initialCapacity=10
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);//initialCapacity=10
}
//調用tableSizeFor()方法
/** * Returns a power of two size for the given target capacity. * 返回指定目標容量的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;
}
複製代碼
下面分析tableSizeFor()
方法:
int n = cap - 1;
爲何要減1操做呢?這是爲了防止`cpa`已是2的冪了。若是`cpa`已是2的冪,又沒有執行減1的操做,則執行完下面的無符號右移後,返回的將爲`cap`的2倍。
複製代碼
|
表示按位或運算:運算規則爲相同二進制位上都是0,結果爲0,不然爲1。第1次運算:
int n = cap - 1;//cap=10,n=9
n |= n >>> 1;//無符號右移1位,而後再與n進行或運算
00000000 00000000 00000000 00001001 //n=9
00000000 00000000 00000000 00000100 //9無符號右移1位變爲4
-----------------------------------------------
00000000 00000000 00000000 00001101 //按位或運算結果爲13,即此時n=13
複製代碼
第2次運算:
int n = 13
n |= n >>> 2;
00000000 00000000 00000000 00001101 //n=13
00000000 00000000 00000000 00000011 //13無符號右移2位變爲3
------------------------------------------------
00000000 00000000 00000000 00001111 //按位或運算結果爲15,即此時n=15
複製代碼
第3次運算:
int n = 15
n |= n >>> 4;
00000000 00000000 00000000 00001111 //n=15
00000000 00000000 00000000 00000000 //15無符號右移4位變爲0
------------------------------------------------
00000000 00000000 00000000 00001111 //按位或運算結果爲15,即此時n=15
複製代碼
接下來的運算結果都是n=15,因爲最後有一個n + 1
操做,最後結果爲16。
總結: 由以上運算過程能夠看出,若是指定的初始容量不是2的n次冪,通過運算後會獲得離初始容量最小的2冪。
private static final long serialVersionUID = 362498820763181265L; //序列化版本號
複製代碼
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //初始化容量,必須是2的n次冪
複製代碼
static final int MAXIMUM_CAPACITY = 1 << 30; //集合最大容量:2的30次冪
複製代碼
static final float DEFAULT_LOAD_FACTOR = 0.75f; //默認的加載因子
/**1.加載因子是用來衡量HashMap的疏密程度,計算HashMap的實時加載因子的方法爲:size/capacity; *2.加載因子太大致使查找元素效率低,過小致使數組的利用率低,默認值爲0.75f是官方給出的一個較好的臨界值; *3.當HashMap裏面容納的元素已經達到HashMap數組長度的75%時,表示HashMap太擠了,須要擴容,而擴容這個過程涉及到rehash、複製數據等操做,很是消耗性能,因此開發中儘可能減小擴容的次數,能夠經過建立HashMap集合對象時指定初始容量來儘可能避免擴容; *4.同時在HashMap的構造方法中能夠指定加載因子大小。 */
HashMap(int initialCapacity, float loadFactor) //構造一個帶指定初始容量和加載因子的空HashMap
複製代碼
static final int TREEIFY_THRESHOLD = 8; //鏈表轉紅黑樹的第一個條件,鏈表長度大於閾值8
複製代碼
static final int UNTREEIFY_THRESHOLD = 6; //刪除紅黑樹節點時,當紅黑樹節點小於6,轉化爲鏈表
複製代碼
static final int MIN_TREEIFY_CAPACITY = 64; //鏈表轉紅黑樹的第二個條件,數組長度大於64
複製代碼
兩個對象的索引相同,而且hashCode(即哈希值)相等時,會發生哈希碰撞。
複製代碼
JDK1.8以前,採用鏈表解決;JDK1.8以後,採用鏈表+紅黑樹解決。
複製代碼
使用equals比較內容是否相同:
相同:後添加的value值會覆蓋以前的value值;
不相同:劃出一個節點存儲(拉鍊法)。
複製代碼
JDK1.8:數組+鏈表+紅黑樹。其中數組是主體,鏈表和紅黑樹是爲解決哈希衝突而存在的,具體以下圖所示:
JDK1.8之前HashMap的底層數據是數組+鏈表,咱們知道,即便哈希函數作得再好,哈希表中的元素也很難達到百分之百均勻分佈。當HashMap中有大量的元素都存在同一個桶(同一個索引位置),這個桶下就會產生一個很長的鏈表,這時HashMap就至關因而一個單鏈表的結構了,假如單鏈表上有n個元素,則遍歷的時間複雜度就是O(n),遍歷效率很低。針對這種狀況,JDK1.8引入了紅黑樹,遍歷紅黑樹的時間複雜度爲O(logn),因爲O(n)>O(logn);因此這一問題獲得了優化。
咱們知道8是從鏈表轉成紅黑樹的閾值,在源碼中有這樣一段註釋內容:
/** 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 */
複製代碼
翻譯過來的的值意思就是說:
紅黑樹節點所佔空間是普通鏈表節點的兩倍,而且鏈表中存儲數據的頻率符合泊松分佈,咱們能夠看到,在鏈表爲8的節點上存儲數據的機率是0.00000006,這也就代表超過8之後的節點存儲數據的機率就很是小了。
由上述分析能夠得出:
若是加載因子是0.4,那麼16*0.4=6,導致數組中滿6個空間就擴容,形成數組利用率過低了;
若是加載因子是0.9,那麼16*0.9=14,這樣就會使數組太滿,很大概率形成某一個索引節點下的鏈表過長,進而致使查找元素效率低;
因此兼顧數組利用率又考慮鏈表不要太長,通過大量測試0.75是最佳值。