java-hashmap源碼分析:建立

本文關鍵知識點

閱讀本文須要15分鐘,能夠先花1分鐘快速瀏覽下面這些知識點是否是很是瞭解。若是有不瞭解的要點再日後看。java

  • 初始大小和加載因子是最重要的參數,new一個hashMap只須要對這兩個field的賦值;
  • initialCapacity K會被hashmap調整成爲最小的大於K的2^N設置爲真實的capacity,默認值爲16最大值爲2^30;對capacity的擴容不能破壞2^N這個基本設計;
  • loadfactor默認爲0.75,threshold=capcity*loadfactor因而默認值是12,當元素個數超過threshold以後map會進行resize;
  • 內部的table採用基本數據結構array來表明buckets,每一個bucket內採用單向鏈表或者紅黑樹來處理髮生哈希衝突的數據;
  • hashmap採用了懶加載的模式,在new對象時僅設置容量和加載因子,table相關的初始化發生在第一次putValue時;
  • hashmap的capacity採用2^n是爲了(1)快速index(2)rehash時最小化成本;也所以擴容的時候也是2倍的擴容,不能破壞這個基本的設計原則;
  • hashmap中的hash方程採用了key原生hashCode高低位xor的設計思路;
  • hashmap中重要的fields:modCount,table,size,threshold,loadfactor,entrySet;
  • hashmap中三類默認配置:容量、加載因子、紅黑樹。

重要參數:initialCapacity,loadFactor;懶加載;

建立一個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。
  • initial capacity和load factor。capacity表明了hash table中有多少個buckets,initial capacity指的是在hash table建立時的大小。
  • load factor是指當hash table多滿的時候會進行自動擴容並從新哈希(rehash),當一個hash table中的元素的個數超過 當前的capcity*loadfactor時,hash table會擴容成原來的2倍並從新哈希。
  • 通用的load factor是0.75,這個取值比較好的平衡了時間和空間的成本。若是加載因子太高,能夠節約內存空間,可是確加劇了大部分hashmap提供操做的搜索的成本,好比get/put。(筆者補充,若是加載因子太小,會浪費空間而且會下降iterator的效率)。

初始容量調整爲2^n

默認的hashmap的大小是16,若是用戶指定了一個數n,在hashmap代碼內部會把它修正成爲不小於n的2的冪。在剛剛的構造器裏能夠看到這裏的賦值並非開發者傳入的值,而是通過了轉化:算法

this.threshold = tableSizeFor(initialCapacity);  // 構造器中的哈希表容量修正
複製代碼

這裏涉及3個問題: 1) hashmap源碼是如何計算容量的; 2) 爲何要讓size是2的冪; 3) 爲了回答第2個問題須要瞭解一個key是如何映射到一個bucket的;編程

capacity 如何調整爲2^n

先看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;
    }
複製代碼

capacity 爲何要調整爲2^n

這裏有兩個緣由(1)爲了key能快速index到bucket;(2)爲了減小resize以後rehash的成本;

1-bitmask代替求餘快速index

這裏爲何要讓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]
複製代碼

2-resize以後低成本的rehash,相似一致性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。

哈希函數xor of higher bits

剛剛在講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的建立

內部字段

  1. hashmap的instance fields中首先是有前面講過的loadfactor和threshold,loadfactor通常在初始化以後就固定了可是threshold在每次hash resize以後會進行從新計算,這二者的關係是threshold=cap*loadfactor。

  2. 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();
複製代碼
相關文章
相關標籤/搜索