天天看點

3. HashMap源碼: hashmap方法(增添節點部分)hashmap方法

hashmap方法

tableSizeFor:擷取最小2整數幂

作用是查找大于等于傳參容量的最小2的整數幂,指派給threshold門檻值。由于擴容是目前數組下标值加上原數組長度,是以需要擴充到原先兩倍大小,又因為初始是16,是以一定擴容後的大小是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;
}
           

作用就是擷取一個滿足大于等于cap(傳入參數,原容量)且是2的整數幂的數之中最小的數。

首先-1是當cap本身就是2的整數幂的時候,滿足包含自身,也就是擴容後的結果還是cap。

|= : n=n| (n >>> 1)

|:左右兩個元素位運算,都是0時該位才是0

n>>> 1: n的二進制向右移動1位,左邊高位空出來的補0

代碼目的就是把n的非0最高位的後面全變成1(0000100變成了0000111)

這樣在+1就獲得了最小的2的整數幂并且大于等于n

hash:判斷hash值(隻考慮key)

  1. ^是異或方法 : 左右值相同是0,不同是1
  2. 1.8 中用的是key的hashcode,注意這裡和高16位做了一個異或,可以防止hash數組太小時,hash值的高位被忽略,也就是太小左
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
           
  1. 桶定位: 用的方法是hash & (table.length -1),也就是hash方法得到的hash值和table數組的長度-1,取并。(相當于h%length)

hashcode, ==, equals, compareTo()差別

可以參考部落格:1,2

== 比較的是變量的句柄存儲位址,而不是實際存儲内容

equals比較的是實際的存儲内容,是以自定義類需要重寫equals方法,否則繼承Object中的equals方法,還是比較句柄位址和==一樣。(因為Object類中的equals隻是判斷兩個引用變量是否引用同一對象,如果不是引用同一對象,即使兩個對象的内容完全相同,也會傳回false。)

hashcode:hashcode是将實體位址轉換成一個int數進行比較,同一位址才相同。

和equals方法沒有強制關系,也需要重寫。保證結果是hashcode相同equals不一定相同,equals為真兩對象相等hashcode一定相等。

compareTo(): 擷取的字元串(也可以是其他對象)的長度,然後作減法,這裡的減法就是ASCII碼的減法,是以compareTo()會傳回數字,如果兩個字元串内容相同,會傳回0,字元串a大于字元串b,會傳回相差的ASCII碼的正數,字元串a小于字元串b,會傳回相差的ASCII碼的負數。

簡而言之就是從長度不同傳回長度差,長度相同傳回編碼內插補點。

public int compareTo(String anotherString) {
        int len1 = value.length;
        int len2 = anotherString.value.length;
        int lim = Math.min(len1, len2);
        char v1[] = value;
        char v2[] = anotherString.value;
 
        int k = 0;
        while (k < lim) {
            char c1 = v1[k];
            char c2 = v2[k];
            if (c1 != c2) {
                return c1 - c2;
            }
            k++;
        }
        return len1 - len2;
    }
           

hashmap中equals傳回true那麼compareTo函數應該也傳回0?

resize:擴容

擴容條件

作用:初始化和擴容table數組,傳回的是Node數組

主要思路:

  1. 先存儲舊(未擴容)數組的長度、門檻值等
  2. 如果舊數組長度大于0

    如果舊數組長度超過或者等于最大容量限制,不能繼續擴容,隻能把門檻值指派為最大整型,傳回舊數組。

    如果擴容後仍然不超過最大容量限制,就擴容一倍,門檻值也翻一倍。

  3. 如果舊數組 為null或者長度等于0

    門檻值大于0,門檻值就是新數組的大小

    門檻值也等于0,就用預設參數來構造新數組

  4. 新門檻值計算
  5. 根據新門檻值和新容量來new一個新的桶數組。
  6. 把舊桶數組的元素複制到新桶數組上

    (1) 如果舊數組桶位上隻有一個節點,直接按照桶定位指派到新數組的索引處

    (2)如果舊數組桶位上存在紅黑樹,那就調用split方法

    (3) 如果是單連結清單,周遊單連結清單,每一個節點根據hash值和舊數組長度的與某位是0還是1.分别劃分到建立的低連結清單/高連結清單當中。劃分結束後把低連結清單放在新數組的同一索引位置;高連結清單在新數組中的索引位置是 舊數組長度+ 舊數組索引。

    ​ (目的就是減少一個單連結清單中的節點數目,配置設定到新數組中的兩個地方去。新數組擴充的舊數組兩倍,是以高連結清單的索引是j + oldCap,相當于一碗高高的米飯平分到了兩碗,當然這兩碗要看成一個新碗。。。哈哈哈)

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table; // 儲存舊(未擴容)的數組
    int oldCap = (oldTab == null) ? 0 : oldTab.length; // 儲存舊數組長度
    int oldThr = threshold; // 儲存舊的容量門檻值
    int newCap, newThr = 0;
    // 舊數組長度大于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; 
    }
    // 舊數組為null,或長度為0時
    else if (oldThr > 0)  // 舊門檻值大于0,就将舊門檻值作為新的數組大小
        newCap = oldThr;
    else { // 舊門檻值為0,舊容量也為0,就采用預設值(使用無參構造器後,插入一個元素就會執行這裡)
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    /* 新門檻值計算 */
    // 新門檻值為0,新門檻值 = 新的長度 * 負載因子
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        // 但如果新的長度或者計算出來的新門檻值超過最大長度限制,就采用最大整型
        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;
    // 舊HashMap中的所有元素配置設定到新的HashMap
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            // 周遊處理每一個桶位中的桶,指派給e
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                // 隻有一個元素的桶,直接指派給新數組
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                // 桶中不止一個元素時
                else if (e instanceof TreeNode) // 紅黑樹就用split方法分離節點
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // 單連結清單就拆分成兩個連結清單
                    Node<K,V> loHead = null, loTail = null; // 原索引處的一個連結清單
                    Node<K,V> hiHead = null, hiTail = null; // 原索引+oldCap的一個連結清單
                    Node<K,V> next;
                    do {
                        next = e.next;
                        // 根據hash值的某一位為0還是1将單連結清單拆分成高低兩個連結清單(高低就是指的0/1)
                        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;
}
           

put和putVal:插入節點(分為單連結清單節點和紅黑樹節點)

put是向hashmap中添加新的鍵值對,或者傳回已有鍵值對中的value值。傳參是key和value。

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
           

主要執行函數是putVal,傳參是待插入鍵值對的key對應的hash方法(hashcode高16位異或)、key、value、後面兩個bool是在linkedhasnmap中需要定義的。傳回值:如果是新加鍵值對,傳回值是null;如果發現存在相同鍵值,直接傳回對應的value值。

主要思路:

  1. 先判斷目前桶數組table的大小,要是未初始化就擴容。
  2. 如果桶定位得到的索引位置沒有連結清單,就直接建立一個節點(和待插入鍵值對字段相同)插入。
  3. 如果目前索引出已經存在連結清單了,在進行判斷

    (1) 如果連結清單的第一個節點的hash和key和待插入鍵值對一樣,直接取其value值

    (2) 如果不一樣,并且是紅黑樹結構,直接調用putTreeVal插入紅黑樹節點。

    (3) 如果hash和key不一樣,是單連結清單結構,就周遊目前連結清單。如果連結清單存在相等key和hash的,就傳回該節點的value,否則在連結清單表尾增加新節點。

    (4) 根據增加新鍵值對後目前單連結清單的長度,判斷是否要擴容或者紅黑樹化。

  4. 如果查到了相同key的鍵值對,就用新的value進行替換,同時傳回舊的value。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 數組未初始化或者長度為0,都要擴容
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 桶定位,如果目前索引位置是null,直接建立添加
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    // 目前索引存在連結清單了
    else {
        Node<K,V> e; K k;
        // 連結清單的頭節點和待插入鍵值對hash、key相同
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // 頭節點不相同,且是紅黑樹結構,直接調用putTreeVal方法。
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        // 目前是單連結清單結構
        else {
            // 周遊單連結清單
            for (int binCount = 0; ; ++binCount) {
                // e = p.next:循環條件 
                if ((e = p.next) == null) {
                    // 表尾新增加鍵值對 ,然後跳出循環
                    p.next = newNode(hash, key, value, null);
                    // 鍵值對數目達到樹化門檻值,判斷是要樹化,還是要擴容
                    if (binCount >= TREEIFY_THRESHOLD - 1)
                        treeifyBin(tab, hash);
                    break; 
                }
                // 周遊中找到相同key的節點,條件中已經指派給了e,是以可以直接跳出循環
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e; // 推動循環
            }
        }
        // 找到了和待插入鍵值對相同key hash 的節點e
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value; // 在這裡用新值替換舊值
            afterNodeAccess(e);  // 空函數
            return oldValue;  // 傳回舊值
        }
    }
    // 此處是新插入了鍵值對,是以hashmap結構變化次數增加
    ++modCount;
    // 鍵值對數量自增,判斷是否擴容
    if (++size > threshold)
        resize(); 
    afterNodeInsertion(evict); // 空函數
    return null;  // 新插入鍵值對,傳回null
}
           

treeifyBin:樹化

作用是把哈希桶(table數組)中索引位置處的Node單連結清單元素轉化為treeNode雙向連結清單元素,并進行紅黑樹化。這裡調用treeify函數的是hd也就是建立的雙向連結清單。是以該函數分為兩部分:1. 建立雙向連結清單,用原單向連結清單指派 2. 用建好的雙向連結清單調用treeify函數紅黑樹化。

final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    //如果Table數組容量太小
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        //調整擴容
        resize();
    //桶定位 擷取索引位置連結清單 目前連結清單是單向 e是Node類型
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        TreeNode<K,V> hd = null, tl = null;
        do {
            //函數作用是new一個TreeNode來代替Node
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;  // 指向連結清單的頭結點,該語句隻執行一次
            else {
                // tl相當于目前連結清單的指針
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        //更換為TreeNode後的雙向連結清單的頭節點 指派給桶數組 如果不為空(連結清單節點存儲了鍵值對),就把連結清單轉變為紅黑樹。
        if ((tab[index] = hd) != null)
            //執行紅黑樹化,主要語句!!
            hd.treeify(tab);
    }
}
           

繼續閱讀