HashMap中的要點

1、HashMap碎碎念node

jdk1.8前插鏈表是頭插法,以後是尾插法算法

key 能夠爲null,hash值爲0數組

紅黑樹弱平衡:併發

紅黑樹是犧牲了嚴格的高度平衡的優越條件爲代價紅黑樹可以以O(log2 n)的時間複雜度進行搜索、插入、刪除操做

2、關鍵變量與默認值app

jdk1.8less

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;  //初始容量爲16
static final int MAXIMUM_CAPACITY = 1 << 30; // 最大容量
static final float DEFAULT_LOAD_FACTOR = 0.75f; // 初始負載因子
static final int TREEIFY_THRESHOLD = 8; // 轉變爲紅黑樹的桶的大小的閾值
static final int UNTREEIFY_THRESHOLD = 6; // 轉爲鏈表的桶的大小閾值
static final int MIN_TREEIFY_CAPACITY = 64; // 桶的閾值被超過期,若數組大小未超過這個,則resize(),超過了則轉爲紅黑樹;這個值要求比TREEIFY_THRESHOLD至少大四倍

 

3、擴容計算不小於當前容量的最小2的n次冪dom

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;
}

4、hash算法函數

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

(1)首先獲取對象的hashCode()值,而後將hashCode值右移16位,而後將右移後的值與原來的hashCode作異或運算,返回結果。(其中h>>>16,在JDK1.8中,優化了高位運算的算法,使用了零擴展,不管正數仍是負數,都在高位插入0)。優化

(2)在putVal源碼中,咱們經過(n-1)&hash獲取該對象的鍵在hashmap中的位置。(其中hash的值就是(1)中得到的值)其中n表示的是hash桶數組的長度,而且該長度爲2的n次方,這樣(n-1)&hash就等價於hash%n。由於&運算的效率高於%運算。this

     tab便是table,n是map集合的容量大小,hash是上面方法的返回值。由於一般聲明map集合時不會指定大小,或者初始化的時候就建立一個容量很大的map對象,因此這個經過容量大小與key值進行hash的算法在開始的時候只會對低位進行計算,雖然容量的2進制高位一開始都是0,可是key的2進制高位一般是有值的,所以先在hash方法中將key的hashCode右移16位在與自身異或,使得高位也能夠參與hash,更大程度上減小了碰撞率。

 

5、putVal 過程

①.判斷鍵值對數組table[i]是否爲空或爲null,不然執行resize()進行擴容;

②.根據鍵值key計算hash值獲得插入的數組索引i,若是table[i]==null,直接新建節點添加,轉向⑥,若是table[i]不爲空,轉向③;

③.判斷table[i]的首個元素是否和key同樣,若是相同直接覆蓋value,不然轉向④,這裏的相同指的是hashCode以及equals;

④.判斷table[i] 是否爲treeNode,即table[i] 是不是紅黑樹,若是是紅黑樹,則直接在樹中插入鍵值對,不然轉向⑤;

⑤.遍歷table[i],判斷鏈表長度是否大於8,大於8的話把鏈表轉換爲紅黑樹,在紅黑樹中執行插入操做,不然進行鏈表的插入操做;遍歷過程當中若發現key已經存在直接覆蓋value便可;

⑥.插入成功後,判斷實際存在的鍵值對數量size是否超多了最大容量threshold,若是超過,進行擴容。

 

6、HashMap 擴容

①.在jdk1.8中,resize方法是在hashmap中的鍵值對大於閥值時或者初始化時,就調用resize方法進行擴容;

②.每次擴展的時候,都是擴展2倍;

③.擴展後Node對象的位置要麼在原位置,要麼移動到原偏移量兩倍的位置。

7、爲何是2倍,2次冪

好處1

在hashmap的源碼中。put方法會調用indexFor(int h, int length)方法,這個方法主要是根據key的hash值找到這個entry在Hash表數組中的位置,源碼以下:

/**
* Returns index for hash code h.
*/
static int indexFor(int h, int length) {
    // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
    return h & (length-1);
}

上述代碼也至關於對length求模。 注意最後return的是h&(length-1)。若是length不爲2的冪,好比15。那麼length-1的2進制就會變成1110。在h爲隨機數的狀況下,和1110作&操做。尾數永遠爲0。那麼000一、100一、1101等尾數爲1的位置就永遠不可能被entry佔用。這樣會形成浪費,不隨機等問題。 length-1 二進制中爲1的位數越多,那麼分佈就平均。

好處2

如下圖爲例,其中圖(a)表示擴容前的key1和key2兩種key肯定索引位置的示例,圖(b)表示擴容後key1和key2兩種key肯定索引位置的示例,n表明length。

元素在從新計算hash以後,由於n變爲2倍,那麼n-1的mask範圍在高位多1bit(紅色),所以新的index就會發生這樣的變化:

resize過程當中不須要像JDK1.7的實現那樣從新計算hash,只須要看看原來的hash值新增的那個bit是1仍是0就行了,是0的話索引沒變,是1的話索引變成「原索引+oldCap」,能夠看看下圖爲16擴充爲32的resize示意圖(一方面位運算更快,另外一方面抗碰撞的Hash函數其實挺耗時的):

這個設計確實很是的巧妙,既省去了從新計算hash值的時間,並且同時,因爲新增的1bit是0仍是1能夠認爲是隨機的,所以resize的過程,均勻的把以前的衝突的節點分散到新的bucket了。這一塊就是JDK1.8新增的優化點。

8、負載因子爲何是0.75

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
*
在理想狀況下,使用隨機哈希嗎,節點出現的頻率在hash桶中遵循泊松分佈,同時給出了桶中元素的個數和機率的對照表。
從上表能夠看出當桶中元素到達8個的時候,機率已經變得很是小,也就是說用0.75做爲負載因子,每一個碰撞位置的鏈表長度超過8個是幾乎不可能的。

負載因子越大則散列表的裝填程度越高,也就是能容納更多的元素,元素多了,鏈表大了,因此此時索引效率就會下降。反之,負載因子越小則鏈表中的數據量就越稀疏,此時會對空間形成爛費,可是此時索引效率高。

9、1.7和1.8的區別

https://blog.csdn.net/qq_36520235/article/details/82417949

 

10、HashMap  putVal源碼解析

 

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
        //這裏的resize  是初始化的時候調用 後面會講
            n = (tab = resize()).length;    //從新計算一下大小
    //獲取要插入元素在 哈希桶中的位置
    if ((p = tab[i = (n - 1) & hash]) == null) //若是這個位置沒有Node
        //   return new Node<>(hash, key, value, next);
        tab[i] = newNode(hash, key, value, null);   //直接建立一個新的Node
        else {  //原來這個桶的位置上有Node
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k)))) //若是你和桶上的第一個Node相等
                e = p;  //直接覆蓋值
            else if (p instanceof TreeNode)//若是 你定位到的元素是一個TreeNode(Node的一個子類,也是HashMap的一個內部類)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);//那麼就插入一TreeNode節點
            else {//定位到這個hash桶了 可是這裏面是鏈表(沒有進行過樹化)
                for (int binCount = 0; ; ++binCount) {//是鏈表
                    if ((e = p.next) == null) {
                    //若是p節點的next爲空 直接在後面插入
                        p.next = newNode(hash, key, value, null);
    //這裏的樹化是putValue的時候 若是原本是鏈表 並且長度超過了8 那麼就進行樹化 
                        if (binCount >= TREEIFY_THRESHOLD - 1) 
                    //** 這個方法一會來分析
                            treeifyBin(tab, hash);
                        break;
                }
//若是下一個節點e 不爲null 而且這個鏈表中的節點就是你要找的節點 終止循環               
                    if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value; //將老的值複製給一個變量準備返回
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;    //將新的值賦值
                afterNodeAccess(e); //這個是空實現
                return oldValue;
            }
        }
            ++modCount;// 修改次數+1 和fastRemove()有關也和併發修改有關
        if (++size > threshold) //若是大於了闕值 須要擴容的大小
            resize();   //從新設置hash桶的大小,也有可能進行樹化,見後面代碼
        afterNodeInsertion(evict);//空方法
        return null;
    }

 

10、HashMap  resize方法源碼解析

final Node<K,V>[] resize() {         Node<K,V>[] oldTab = table; // oldTable:當前的表         int oldCap = (oldTab == null) ? 0 : oldTab.length;  //若是你是新建立的話 表的大小就是0 不然就是原來的大小         //第一次是爲0的    表明 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16         int oldThr = threshold;              int newCap, newThr = 0; //新的容量和新的擴容         //若是舊的容量大於0         if (oldCap > 0) {         //若是舊的容量大於最大的容量             if (oldCap >= MAXIMUM_CAPACITY) {                //那麼擴容大小 = 最大範圍                 threshold = Integer.MAX_VALUE;               //直接返回了                 return oldTab;               }         //不然 若是新的大小等於 oldCap * 2 < 最大的容量 , 而且舊的容量大於默認的初始化大小16             else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&                      oldCap >= DEFAULT_INITIAL_CAPACITY)      // double threshold  新的擴容 = 舊的擴容 * 2                  newThr = oldThr << 1;         }         else if (oldThr > 0) // initial capacity was placed in threshold              newCap = oldThr;    //若是舊的擴容原本就大於0,那麼新的容量就是舊的擴容         else {               // zero initial threshold signifies using defaults 說明是 threshold爲0的時候的狀況             newCap = DEFAULT_INITIAL_CAPACITY;      //新的容量爲默認容器的容量             newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); //新的闕值爲 默認的容量 * 負載因子         }            if (newThr == 0) {  //若是新的擴容爲0              float ft = (float)newCap * loadFactor;  //計算獲得新的闕值         //新的闕值 =  若是新的容量小於 最大的容量 而且 新的闕只 < 最大的容量 那麼新的闕值 = 計算的 不然 = 最大int             newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?                       (int)ft : Integer.MAX_VALUE);         }         threshold = newThr; //闕值 = 新的闕值         @SuppressWarnings({"rawtypes","unchecked"})             Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//建立一個新的哈希數組桶 大小爲新的容量             table = newTab; //          if (oldTab != null) {     //遍歷舊的hash桶         for (int j = 0; j < oldCap; ++j) {                   Node<K,V> e;                     if ((e = oldTab[j]) != null) {//若是舊的hash桶的元素不爲null  e爲舊的hash桶的元素                     oldTab[j] = null;   //舊的hash桶設置爲null                     if (e.next == null)     //若是你就是一個元素                         newTab[e.hash & (newCap - 1)] = e;  //那麼在新的hash桶給你安排一個位置  位置是你的hash值 & 新的桶的容量-1 這至關於 你的hash值 與 你的容量進行取模運算                     else if (e instanceof TreeNode) //若是你不僅一個元素而且是TreeNode                         ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);//分割  將樹中的節點 分割到高位或者地位上去                       else { // preserve order        //是普通的鏈表                         Node<K,V> loHead = null, loTail = null;                          Node<K,V> hiHead = null, hiTail = null;                          Node<K,V> next;                          do {                                 next = e.next;                             if ((e.hash & oldCap) == 0) {   //看是否須要進行位置變化 新增位的值 不須要變化就放在原來的位置                                 if (loTail == null)                                     loHead = e;                                  else                                     loTail.next = e;                                 loTail = e;                             }                             else {      //須要變化 就構建高位放置的鏈表                                 if (hiTail == null)                                     hiHead = e;                                 else                                     hiTail.next = e;                                 hiTail = e;                             }                         } while ((e = next) != null);                         if (loTail != null) {                             loTail.next = null;                             newTab[j] = loHead; //賦值 (原來位置)                         }                         if (hiTail != null) {                             hiTail.next = null;                             newTab[j + oldCap] = hiHead;//在新鏈表的位置賦值                         }                     }                 }             }         }         return newTab;     }

相關文章
相關標籤/搜索