閱讀本文須要15分鐘,能夠先花1分鐘快速瀏覽下面這些知識點是否是很是瞭解。若是有不瞭解的要點再日後看。java
建立一個hashmap最基本的構造器須要指定一個hashmap的初始大小和loadfactor。固然這個構造器你們其實不經常使用,hashmap在這個本質的構造思路的基礎上包裝了:隱藏loadFactor(使用默認值0.75),隱藏initCapacity(默認16)和loadFactor的構造器。固然也能夠根據一個map直接建立對象並putValue。node
從最本質的構造器能夠發現new一個map其實只須要給兩個定義hashmap的關鍵內部變量賦值,其餘的什麼都不用多作。這實際上是一種lazy的思想,new對象時只記錄定義這個數據結構的關鍵參數,而不進行初始化,在真正須要使用時再進行初始化。(這裏根據其餘map來構造除外,由於這個時候已經有了elements to put)。面試
// 最本質的構造器
public HashMap(int initialCapacity, float loadFactor) {
// 核心參數initCapacity: 最小是0,有個最大限制1 << 30 (1,073,741,824大概是一百多萬)
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// 核心參數loadFactor: 必須設置大於0的數,畢竟這個數表明了
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
// 包裝或者變化以後更容易用的3個構造器
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
複製代碼
關鍵的兩個變量java doc解釋很是清楚,這裏簡化翻譯一下。redis
HashMap有兩個影響其性能的變量,初始大小initial capacity和加載因子 load factor。默認的hashmap的大小是16,若是用戶指定了一個數n,在hashmap代碼內部會把它修正成爲不小於n的2的冪。在剛剛的構造器裏能夠看到這裏的賦值並非開發者傳入的值,而是通過了轉化:算法
this.threshold = tableSizeFor(initialCapacity); // 構造器中的哈希表容量修正
複製代碼
這裏涉及3個問題: 1) hashmap源碼是如何計算容量的; 2) 爲何要讓size是2的冪; 3) 爲了回答第2個問題須要瞭解一個key是如何映射到一個bucket的;編程
先看hashMap內部哈希表的大小是如何計算的。這裏DougLea大哥直接使用了位運算,果真大佬們都是用位運算的(寫redis的antirez也通篇用位運算)。 下面的無符號右移運算的結果是取得了一個11...111(二進制表示法)的數,二進制位數與cap的位數相同。你們能夠本身寫一個數嚴格按這個操做試試。bootstrap
這裏解釋一下爲何右移的位數是每次*2的,由於第一次右移必定能保證n的最高位和次高位都是1(n的最高位必定是1,第一次按位或使得次高位不管是什麼也必定是1),這樣就保證了原來數的最前面兩個bit是1,下次用這兩個1去按位或就好,這樣下一輪能夠把2個bit肯定爲1,在下一輪4個。每次確認bit的個數是原來的2倍,這是一種增速算法。bash
在此基礎上+1,就有了一個必定比initial cap大的2^k的容量。數據結構
/**
* 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;
}
複製代碼
這裏爲何要讓cap是2^n,是由於hashmap這裏採用了bitmask的方法來代替除法求餘哈希。求餘運算是一種CPU低效率的運算,相反位運算就快速的多了,因此bitmask就是一種常見的替代思路。bitmask正如其名,去mask掉(掩蓋掉)bit值,留下須要的值。app
舉個栗子來講,一個hashtable的長度是4(2^2),那麼buckets用二進制表示就是00,01,10,11。那麼對key一個很簡單的分配bucket的方法就是0000...00011&key,也就是看key的最後兩位屬於哪一個範疇。這裏hashmap源碼也是這麼實現的,
這裏hash是key在通過hash function計算的值,tab是hashmap中的Node[](array)與n-1按位與計算獲得index;
tab[i = (n - 1) & hash]
複製代碼
這裏首先要知道resize是編程原來capacity的兩倍,遵循了capacity是2整數次冪的設定。這裏的設計和arrayList是不一樣的,arraylist看一下源碼能夠粗糙的認爲擴容是1.5倍,採用了oldCap+oldCap>>1的方案。至於我看還有面試官喜歡問爲何二者不同,緣由主要是hashmap 2^n的設計不能由於擴容而破壞(固然能夠進一步回答爲何要這麼設計),此外hashmap擴容能夠減小hash碰撞對查詢效率是有幫助的,而arraylist擴容若是沒有真的填寫更多數據就形成了浪費,因此保守選了1.5倍。
一致性hash但願達到的目的是hash nodes發生變化時,rehash最小化須要遷移的數據。而經過使用上述2^n的table容量,和(n - 1) & hash的index定位方法,hashmap在resize到兩倍時能保證:1)一部分數據不須要遷移;2)須要遷移和不須要遷移的數據能快速確認;(注意者不表明resize是低成本的,只是在一個比較高成本的操做上儘量減小其成本)。
resize的源碼分析放在後面,這裏先講一下原理:index=(n-1)&hash,當n-1的二進制表示從0011變成兩倍的0111時,一個key的hash只有在新增的一個最高位1有值時纔會被hash到新的位置,不然就保持不動。那麼如何這個1呢?0100&hash就能夠了,這個簡單的位運算很是快,而0100其實就是原來的容量值old Capacity。
剛剛在講bitmask的方法確認index時,參與 index = a&(n-1) 的並非key自己,而是其hash值。hashmap相關的數據結構最重要可是卻最容易被忽略的就是hashfunction,若是hashfunction選擇的很差每每這個數據結構設計的再好也無法用,然而hash每每是數據家研究的領域工程師們每每瞭解的比較粗淺。不過略懂也是好的。
看一下源碼:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
複製代碼
這裏第一個信息點事能夠看到hash支持key是null,會hash到0。 第二個信息點是key的自定義的hash值進行位移後的異或操做,這裏code的做者作了解釋,這裏翻譯一下:
在前面bitmasking方案中,因爲hashtable的長度關係,一個key的hashCode每每只有最低的一些bit位參與了運算,若是兩個hashCode只有比mask高位bit有變化,那麼在這種index方案下就必定會發生哈希碰撞。小的hashtale若是key是連續的浮點數的話,這種哈希碰撞的方案很是常見。因此咱們提出了一個讓高位bits參與到hash值的方案,這個方案權衡了計算速度、可用性和bit分散的程度。因爲大部分hashCode可能已經足夠分散了,而且咱們也用了tree來處理哈希碰撞,因此咱們就用了右移高位bits進行XOR運算這個廉價的方案來避免系統性的失敗。
這裏再總結一下從一個key到哈希表有如下過程: 1. key.hashCode() 2. 再次hash (>>>16, xor) 3. hash & (cap-1) 獲得index 4. index中node的建立hashmap的instance fields中首先是有前面講過的loadfactor和threshold,loadfactor通常在初始化以後就固定了可是threshold在每次hash resize以後會進行從新計算,這二者的關係是threshold=cap*loadfactor。
size字段來記錄key-value的個數,每次putVal發生時size++,維護這個字段方便程序判斷是否size超過了threshold的限制須要進行擴容。
3)modCount就是一個常見且很是巧妙的設計了,這裏有必要看一下源碼大佬們的原話: modCount用來表示hashmap結構性的修改,結構性的修改指的是影響hashmap中mapping個數的修改,或者直接改變hashmap內部結構的修改(好比rehash)。這個字段的維護是爲了幫助iterator和views能快速失敗,避免讀到不正確的數據(ConcurrentModificationException)。
這裏看一下putVal過程就會改變剛剛講過的size和modCount的數值。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
...//中間省略了
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
複製代碼
4) hashmap中哈希表的具體實現table內部使用了兩種基本數據結構:array和基於node的單向鏈表。hashmap其實就兩大類解決哈希衝突的方式,或者說兩大類實現方式:碰撞採用鏈表或者tree的數據結構,或者碰撞採用開放尋址。而java使用了第一種的方案。
/* ---------------- Fields -------------- */
/** 這裏代表table的初始化發生在第一次使用的時候,resize也發生在須要的時候,並不會提早操做。同時table size是2^n。
* The table, initialized on first use, and resized as
* necessary. When allocated, length is always a power of two.
* (We also tolerate length zero in some operations to allow
* bootstrapping mechanics that are currently not needed.)
*/
transient Node<K,V>[] table;
複製代碼
5) node的定義就比較複雜了,直接間接涉及到hashmap的有三個:單向鏈表,雙向鏈表,TreeNode。這裏就不貼所有代碼了,看一下核心字段: /** * 最基本的單向鏈表Node */ static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
// 在linked hashmap中的雙向鏈表,因此與Node惟一的區別只是指針的區別
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
// treeNode是最複雜的結構,由於紅黑樹的算法比鏈表要複雜多了,本篇就不大篇幅展開分析了,須要在講treefy和untreefy中嘗試講解。
複製代碼
static field主要有咱們以前講過的:initial_capacity默認值是16,最大的容量默認2^30大概是100多萬,默認加載因子是0.75。上面對於這兩個值已經充分討論過了,就很少贅述了。
// 容量相關
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final int MAXIMUM_CAPACITY = 1 << 30;
// 加載因子相關
static final float DEFAULT_LOAD_FACTOR = 0.75f;
複製代碼
剩下3個默認值都與紅黑樹相關。鏈表雖然搜索複雜度是O(n)比紅黑樹的O(logN)滿,可是鏈表的操做簡單,因此hashmap只有當一個bucket內部不斷putValue直到nodes超過必定數量時纔會考慮把鏈表轉換爲紅黑樹,這個默認數值TREEIFY_THRESHOLD是8;固然哈希碰撞變多的另外一個緣由是table過小;因此hashmap的另外一個限制是table中buckets的個數也就是table的長度,若是table比較小那麼優先選擇對整個hashtable進行擴容而不是改形成樹,這個默認的table大小是64。
若是一個bucket已經轉換成紅黑樹,可是一個bucket內部數量不斷減小,少於一個下下限時hashmap會認爲鏈表更經濟把樹再轉換爲鏈表,這個下限默認是UNTREEIFY_THRESHOLD=6。
6個核心static字段
// 紅黑樹相關
static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;(進行紅黑樹轉換的最小table長度)
// treefy的判斷前提
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
複製代碼