Java8的HashMap原理學(xué)習(xí)

HashMap是一個由數(shù)組加鏈表(紅黑樹)組成的散列表低剔,可以非常方便的存儲KV數(shù)據(jù),也可以插入K為null的信息,那么HashMap有哪些不足之處需要注意的呢襟齿?HashMap的擴(kuò)容機(jī)制又是如何姻锁;常見的put、get方法工作細(xì)節(jié)又是如何猜欺,帶著這一系列問題來學(xué)習(xí)一下JDK1.8里面的HashMap的源碼屋摔,了解其工作原理。

HashMap 構(gòu)造函數(shù)
HashMap get方法
HashMap put方法
HashMap resize方法
HashMap remove方法
HashMap 迭代器iterator
問題&總結(jié)
什么時候會創(chuàng)建table數(shù)組?
table數(shù)組長度可以為任意數(shù)字么替梨?
為什么table數(shù)組長度必須為2^n?
為什么默認(rèn)的負(fù)載因子0.75f钓试?
有什么缺點么?

先了解下HashMap中的幾個比較關(guān)鍵的成員變量副瀑,后續(xù)關(guān)于get和put的方法的細(xì)節(jié)都脫離不了這幾個參數(shù)的配合

// 默認(rèn)的HashMap table 數(shù)組大小 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

// 數(shù)組最大值弓熏,2^30 
static final int MAXIMUM_CAPACITY = 1 << 30;

// 默認(rèn)的負(fù)載因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;

// 鏈表轉(zhuǎn)換為紅黑樹的節(jié)點最小個數(shù)(當(dāng)鏈表長度大于等于8時會從鏈表轉(zhuǎn)變?yōu)榧t黑樹)
static final int TREEIFY_THRESHOLD = 8;

// 紅黑樹經(jīng)過remove后節(jié)點個數(shù)小于等于該值時 從紅黑樹蛻變?yōu)殒湵?static final int UNTREEIFY_THRESHOLD = 6;

// HashMap的數(shù)組,是Node類型的糠睡,初始值為null
transient Node<K,V>[] table;

// 在迭代器中使用挽鞠,可依次遍歷Hashmap的節(jié)點
transient Set<Map.Entry<K,V>> entrySet;

// HashMap的節(jié)點個數(shù)
transient int size;

// HashMap的修改次數(shù),主要用在fail-fast中
transient int modCount;

// HashMap閥值狈孔,當(dāng)size超過閥值時進(jìn)行擴(kuò)容操作信认,一般情況下閥值 = 數(shù)組的長度 * 負(fù)載因子
int threshold;

// 負(fù)載因子,默認(rèn)是0.75f均抽,可以認(rèn)為是限制HashMap的碰撞因子
final float loadFactor;

HashMap 構(gòu)造函數(shù)

public HashMap(int initialCapacity, float loadFactor) {
    // 傳入默認(rèn)初始化的容量大小參數(shù)
    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);
    // 這里有個比較重要的方法tableSizeFor
}

public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
    // 同樣是傳入了自定義的容量大小以及默認(rèn)負(fù)載因子
}

public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; 
    // 所有的信息都是默認(rèn)的嫁赏,加載因子設(shè)置為0.75f
}

這有個疑問,initialCapacity 參數(shù)一般是當(dāng)做HashMap的初始化大小使用油挥,可是細(xì)看代碼得之并沒有設(shè)置成table的初始長度潦蝇,反而是通過tableSizeFor方法計算table默認(rèn)的閥值threshold的值

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

>>> 含義是無符號的右移深寥,那么這整個函數(shù)的作用是什么呢?現(xiàn)在假設(shè)n對應(yīng)的二進(jìn)制是1XXX XXXX

右移動1位惋鹅,則有01XX XXXX,進(jìn)行一次異或運算 得到 11XX XXXX
再移動2位,則有0011 11XX,進(jìn)行一次異或運算 得到 1111 11XX
再移動4位闰集,則有0000 1111,進(jìn)行一次異或運算 得到 1111 1111

現(xiàn)在看下經(jīng)過了1,2妥泉,4三個運算,就相當(dāng)于把當(dāng)前已知最大的1右邊的1 + 2 + 4位全部改成了1盲链,變成了最大值
所以現(xiàn)在這幾次異或運算就是把當(dāng)前二進(jìn)制位最大的1右邊的0全部改成1(因為移動了1 + 2 + 4 + 8 + 16 = 31位),現(xiàn)在計算出來的n就是0000 0000 0001 1111這種格式的數(shù)據(jù)刽沾,再進(jìn)行加一操作也就成為了 0010 0000(是個偶數(shù))本慕,現(xiàn)在假設(shè)傳入了的cap=12,那么就是1100侧漓,經(jīng)過異或運算就變成了1111锅尘,再+1處理就成為了 0001 0000,也就是16布蔗。所以現(xiàn)在知道了其實這個方法就是為了計算出比傳入數(shù)據(jù)大且最接近的2^n的數(shù)組

那么還有一個問題藤违,為什么一開始減1呢?其實是為了處理當(dāng)cap=0的情況纵揍,當(dāng)cap=0時顿乒,計算的數(shù)據(jù)為1,同樣滿足1 = 2^0且大于0的結(jié)論

所以如果傳入了initialCapacity泽谨,threshold閥值一定會是2^n這種數(shù)據(jù)的
調(diào)用HashMap()方法璧榄,則閥值和容量都是0
調(diào)用HashMap(initialCapacity) 方法,則賦值閥值吧雹,容量為0
當(dāng)然這里還是沒有解釋上面的一個疑問「table的長度到底是多少」骨杂,繼續(xù)往下看

HashMap get方法

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    // 隱含了當(dāng)key為null的時候返回0這種情況
    // 剩下的都是hash的高16位和低16位進(jìn)行異或運算,其原因也是為了降低hash沖突
}

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        // 如果tab不為null雄卷,而且長度>0搓蚪,再甚至數(shù)組偏移量的節(jié)點first不為null
        // 偏移量是通過(tab.length - 1)&hash計算出來的
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            // 當(dāng)前first節(jié)點如果K值一樣,直接返回
            return first;
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                // 紅黑樹龙亲,查詢紅黑樹的節(jié)點信息
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
                    // 依次遍歷陕凹,計算hash值一樣,且Key一樣的節(jié)點
            } while ((e = e.next) != null);
        }
    }
    return null;
}

需要關(guān)注一下鳄炉,在定位數(shù)組的偏移量是采用了(tab.length - 1)&hash計算的,因為tab.length = 2^n,那么一定可以確保偏移量在數(shù)組之內(nèi)

HashMap put方法

public V put(K key, V value) {
    // 先計算hash值搜骡,然后調(diào)用putVal方法
    return putVal(hash(key), key, value, false, true);
}

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)
        // 如果table為null或者table長度為0拂盯,需要進(jìn)行重置table操作(后面再說resize方法)
        // 初始化時,table為null记靡,也是一種Lazy的思想
        n = (tab = resize()).length;
        
    // 現(xiàn)在table是有一定長度的數(shù)組,而且長度為2^n
    // 定位數(shù)字的索引 (n-1) & hash 
    // 01111 1111 和hash值的和運算
    if ((p = tab[i = (n - 1) & hash]) == null)
        // 數(shù)組上節(jié)點為null空凸,表示沒有數(shù)據(jù)寸痢,直接創(chuàng)建一個新節(jié)點設(shè)置好就可以了
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        // 此時節(jié)點p是數(shù)組有值的節(jié)點,也可以是鏈表(紅黑樹)的首節(jié)點
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            // hash值一樣兵罢,key值也一樣卖词,那就相當(dāng)于發(fā)現(xiàn)了K一樣的情況
            e = p;
        else if (p instanceof TreeNode)
            // p節(jié)點是樹節(jié)點的類型吏夯,添加到紅黑樹中去
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            for (int binCount = 0; ; ++binCount) {
                // 遍歷鏈表的節(jié)點
                if ((e = p.next) == null) {
                    // 遍歷到最后一個節(jié)點了噪生,肯定為null
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        // 記住binCount是從0開始計數(shù)的,所以這里是TREEIFY_THRESHOLD - 1 = 7
                        //  當(dāng)鏈表節(jié)點數(shù)目大于等于8顾瞪,則會轉(zhuǎn)變成紅黑樹
                        treeifyBin(tab, hash);
                    break;
                    // 鏈表執(zhí)行完了肯定就退出循環(huán)
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    // 鏈表遍歷中發(fā)現(xiàn)了同樣的K值節(jié)點
                    break;
                p = e;
                // 否則繼續(xù)遍歷
            }
        }
        if (e != null) { 
            // 如果找到了同樣的K的節(jié)點信息
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
               // 替換老的Value信息
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
            // 返回舊的Value信息
        }
    }
    // 到這里來的都算需要往Map中添加新節(jié)點的
    // 所以modCount + 1操作
    ++modCount;
    if (++size > threshold)
        // HashMap的節(jié)點大小個數(shù)超過了閥值陈醒,調(diào)用resize
        resize();
    afterNodeInsertion(evict);
    // 新增節(jié)點钉跷,肯定就沒有舊節(jié)點信息爷辙,返回null完事
    return null;
}

整個流程只是需要注意resize()方法朦促,當(dāng)table還未初始化和當(dāng)前的HashMap節(jié)點數(shù)超過閥值時就會調(diào)用resize進(jìn)行擴(kuò)容操作务冕,那么現(xiàn)在就來看下resize方法

HashMap resize方法

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    // 在初次調(diào)用put時禀忆,table就是null,設(shè)定oldCap = 0离熏,否則就是舊table的長度
    int oldThr = threshold;
    // 舊閥值oldThr
    int newCap, newThr = 0;
    if (oldCap > 0) {
        // 如果老的table長度大于0
        if (oldCap >= MAXIMUM_CAPACITY) {
            // 如果數(shù)組長度大于等于容量最大值滋戳,只會修改閥值大小,不進(jìn)行擴(kuò)容操作
            // 即使發(fā)生碰撞情況也不再考慮
            // 換句話說矢棚,在Map裝入了如此多的數(shù)據(jù)也不太合理吧
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
                 // 新的table容量翻倍處理
                // 如果oldCap * 2 小于最大容量 而且 oldCap 超過默認(rèn)16個容量大小府喳,新的閥值也會翻倍
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
       // 調(diào)用了HashMap(int, float) 的構(gòu)造方法的初始情況
        // 舊table的長度為0 且 舊閥值大于0(可以理解為初始情況)
        // 而且新閥值這個時候依舊等于0钝满,無變化
        // 注意這點弯蚜!新的table長度是舊的閥值,而之前已經(jīng)說了閥值數(shù)據(jù)是2^n路鹰,直接賦值給了table
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        // 調(diào)用了HashMap()的構(gòu)造方法的初始情況
        newCap = DEFAULT_INITIAL_CAPACITY;
        // 新的默認(rèn)table長度是16
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        // 新的閥值 = 16 * 0.75晋柱,這里才是之前一直所說的閥值=0.75的容量情況
    }
    
    // 新的閥值為0诵叁,只有oldThr > 0 這一種情況下才有可能
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
        // 對閥值進(jìn)行重新計算操作
    }
    threshold = newThr;
     // 所以才有了常說的 閥值 = 0.75 * table長度
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    // 新創(chuàng)建了一個新容量的newTab拧额,接下來就是開始拷貝搬遷舊table的數(shù)據(jù)了
    table = newTab;
    if (oldTab != null) {
        // 舊table有過值的情況下侥锦,依次開始遍歷操作
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
                    // 如果該節(jié)點沒有外接鏈表或者紅黑樹,直接重新計算偏移量賦值即可
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                   // 樹節(jié)點的重新切割處理
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    // 開始處理鏈表了泪幌,此時e節(jié)點還未處理
                    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) {
                            // 按照和運算 是否為0進(jìn)行拆分
                            if (loTail == null)
                                // 當(dāng)前e節(jié)點賦值給loHead
                                loHead = e;
                            else
                                loTail.next = e;
                            // loTail也賦值給了e節(jié)點
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    // 經(jīng)過上面的while循環(huán)處理,就是相當(dāng)于把節(jié)點hash是否為0拆分為2種情況
                    // 各自由一個鏈表負(fù)責(zé)建芙,采用的尾插法
                    if (loTail != null) {
                        // 和運算為0的情況禁荸,直接掛載在原偏移量節(jié)點下
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                       // 否則,掛載在j + oldCap 的節(jié)點下
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                } 
            }
        }
    }
    return newTab;
}

resize方法整體而言還是比較復(fù)雜的瑰妄,涉及到了的調(diào)用場景有無參構(gòu)造映砖、有參構(gòu)造邑退、正常擴(kuò)容等三種情況,所以也就有了開頭比較復(fù)雜的新容量大小以及新閥值的計算行為蜈七。

無參構(gòu)造:新容量是默認(rèn)容量值16飒硅,新閥值是默認(rèn)容量值16 * 負(fù)載因子0.75 = 12
有參構(gòu)造:新容量是由傳入的initialCapacity數(shù)據(jù)計算出大且最近的2^n的值作谚,新閥值是新容量 * 0.75(如果新容量值或者新閥值超過了 最大的容量值,修正新閥值為Integer.MAX_VALUE)
正常擴(kuò)容:舊容量大于等于最大容量值尽棕,設(shè)置新閥值為Integer.MAX_VALUE滔悉,結(jié)束擴(kuò)容单绑;否則新容量double操作(有一些信息被忽略了)

再來細(xì)說一下e.hash & oldCap) == 0這個判斷邏輯搂橙,我們知道table的數(shù)組偏移量的計算是通過hash & (oldCap-1)得到的,既然是在同一個鏈表中苔巨,則其結(jié)果是相同的侄泽,又因為oldCap = 2^n蜻韭,所以意味著不同的節(jié)點Node1和Node2 的hash值必然存在著后幾位為1的bit一定相等的情況(和此處邏輯無關(guān)緊要),例如如下結(jié)果做和運算

Node1 0101  1010
Node2 0001  1010
len-1 0000  1111
table 0001  0000

這兩個節(jié)點后四位一定會相同的闺魏,所以也就是看table二進(jìn)制為1的那1bit和節(jié)點相對應(yīng)的那1bit的和運算是否為0進(jìn)行拆分,這樣可盡可能進(jìn)行均等拆分司草,降低hash沖突翻伺,提高效率沮焕。

HashMap remove方法

public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value;
    // 找到了待移除的節(jié)點e峦树,返回其value,否則返回null
}

final Node<K,V> removeNode(int hash, Object key, Object value,
      boolean matchValue, boolean movable) {
    // matchValue 為true時會比對找到的節(jié)點和傳入的Value 值
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        Node<K,V> node = null, e; K k; V v;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
        else if ((e = p.next) != null) {
            if (p instanceof TreeNode)
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            else {
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        // 上面就不再細(xì)說了急灭,就是找到待移除的節(jié)點node
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
            // 如果node節(jié)點不為null 而且
            // 1葬馋、無需匹配Value 值
            // 2畴嘶、需要匹配且value值確實相同
            // 上述1 和 2 是或的關(guān)系
            if (node instanceof TreeNode)
               // 樹節(jié)點的移除方法
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            else if (node == p)
                // node和p相同就意味著node節(jié)點是table上的窗悯,直接把node的next賦值給table即可
                tab[index] = node.next;
            else
                // p節(jié)點是node節(jié)點的前節(jié)點
                // 直接把node的next賦值給p的next節(jié)點 
                p.next = node.next;
            ++modCount;
            --size;
            afterNodeRemoval(node);
            // hashmap的修改次數(shù)+1偷拔,size減一操作,然后返回node節(jié)點
            return node;
        }
    }
    return null;
}

這里有個小點需要注意到在鏈表的首節(jié)點(也是table上)和鏈表中的節(jié)點移除邏輯是不一樣的欺旧,鏈表中需要知道其移除節(jié)點的上一個節(jié)點切端,而首節(jié)點則沒有上一個節(jié)點這種說法顷啼,直接把下一個節(jié)點賦值給table就行了

HashMap 迭代器iterator

HashMap在遍歷輸出時钙蒙,經(jīng)常使用到迭代器Iterator,常見的代碼如下

在JDK1.8中有一個forEach(BiConsumer<? super K, ? super V> action)也可以很方便的處理HashMap

Map<String, String> map = new HashMap<>();

map.put("1111", "2222");
map.put("2222", "44444");
map.put("3333", "66666");

Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
    Map.Entry entry = iterator.next();
    System.out.print("key:" + entry.getKey() + ", value: " + entry.getValue());
}

獲取EntrySet迭代器马昨,然后利用hasNext以及next方法獲取具體的值鸿捧,代碼很簡單疙渣,看看HashMap如何實現(xiàn)其步驟的

public Set<Map.Entry<K,V>> entrySet() {
    Set<Map.Entry<K,V>> es;
    return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
    // 沒做什么只是直接創(chuàng)建了一個新的EntrySet對象
}

final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
    public final int size()                 { return size; }
    public final void clear()               { HashMap.this.clear(); }
    public final Iterator<Map.Entry<K,V>> iterator() {
        // 調(diào)用EntrySet的iterator也是啥都沒做妄荔,返回了一個EntryIterator新對象,使用的是無參構(gòu)造
        return new EntryIterator();
    }
    .....
}

那現(xiàn)在來看看EntryIterator類到底干了什么

// Hash迭代器抽象類
abstract class HashIterator {
    Node<K,V> next;        // next entry to return
    Node<K,V> current;     // current entry
    int expectedModCount;  // for fast-fail
    int index;             // current slot

    HashIterator() {
        expectedModCount = modCount;
        // 記錄下了當(dāng)前的修改次數(shù)哗伯,用于快速失敗
        Node<K,V>[] t = table;
        current = next = null;
        index = 0;
        if (t != null && size > 0) { 
            // 空轉(zhuǎn)焊刹,從數(shù)組的第一個開始遍歷恳蹲,直到節(jié)點不為null為止阱缓,記錄下index和next
            do {} while (index < t.length && (next = t[index++]) == null);
        }
    }

    public final boolean hasNext() {
        // 就是看下一個節(jié)點next是否為null
        return next != null;
    }

    final Node<K,V> nextNode() {
        Node<K,V>[] t;
        Node<K,V> e = next;
        if (modCount != expectedModCount)
            // 在迭代期間hashmap經(jīng)歷了修改操作,直接拋出異常
            throw new ConcurrentModificationException();
        if (e == null)
            // 意味著必須先調(diào)用hasNext敞嗡,通過后才可調(diào)用nextNode
            throw new NoSuchElementException();
        if ((next = (current = e).next) == null && (t = table) != null) {
            // 把當(dāng)前處理的節(jié)點賦值給current丰榴,開始遍歷鏈表
            // 直到鏈表的下一個節(jié)點為null時(也就意味著鏈表遍歷完成),需要進(jìn)入到下一個table節(jié)點遍歷
            do {} while (index < t.length && (next = t[index++]) == null);
        }
        return e;
    }

    public final void remove() {
        Node<K,V> p = current;
        if (p == null)
            throw new IllegalStateException();
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        current = null;
        K key = p.key;
        removeNode(hash(key), key, null, false, false);
        // 移除節(jié)點信息,同時賦值新的修改次數(shù)給expectedModCount
        // 也就意味著如果想在迭代中移除節(jié)點勺像,只能通過remove方法
        expectedModCount = modCount;
    }
}

final class EntryIterator extends HashIterator
    implements Iterator<Map.Entry<K,V>> {
    // 繼承了HashIterator 實現(xiàn)了迭代器的接口
    public final Map.Entry<K,V> next() { return nextNode(); }
}

整個的迭代過程也是很清晰的,先預(yù)處理遍歷table找到第一個不為null的節(jié)點吟宦,后面遍歷該節(jié)點的next值殃姓,直到為null也就意味著該節(jié)點的鏈表(紅黑樹)遍歷完成,繼續(xù)遍歷table下一個節(jié)點篷牌。

問題&總結(jié)

什么時候會創(chuàng)建table數(shù)組踏幻?

HashMap使用的Lazy模式叫倍,初始化時不會創(chuàng)建table,而是直到put數(shù)據(jù)時才會創(chuàng)建table數(shù)組

table數(shù)組長度可以為任意數(shù)字么听诸?

不可以蚕泽,通過tableSizeFor方法,一定會修正使得table的length為2^n仔蝌,所以new HashMap(12) 和new HashMap(13)其實是一樣的含義敛惊,最后length都會被修正為16
建議給新建的HashMap賦予一定的初始值绰更,減少不必要的擴(kuò)容操作,損耗性能

為什么默認(rèn)的負(fù)載因子0.75f特恬?

這個點在HashMap的源碼描述中已經(jīng)介紹了癌刽,hash的值是遵循泊松分布,當(dāng)負(fù)載因子為0.75時衡奥,碰撞出現(xiàn)鏈表長度大于等于8個情況概率小于1/1kw讼油,也恰好說明了為什么鏈表轉(zhuǎn)為紅黑樹的節(jié)點個數(shù)為8,當(dāng)真正出現(xiàn)紅黑樹也說明了hash碰撞比較嚴(yán)重,所以使用紅黑樹提高效率
至于為什么是泊松分布就涉及到概率統(tǒng)計學(xué)了瘦赫,可以翻翻相關(guān)的paper
這個0.75也無絕對蛤迎,只是針對大部分場景比較適用,也是對空間和時間的一種妥協(xié)校辩,如果你能嚴(yán)格證明0.8甚至0.9可以提高自身業(yè)務(wù)的效率辆童,也是可以傳入自定義的負(fù)載因子

為什么table數(shù)組長度必須為2^n?

進(jìn)行數(shù)組偏移量計算時把鉴,采用的是(n-1) & hash,n-1又是0001111 類似尾部全部為1的情況场晶,使得計算出來的值一定在table的長度范圍之內(nèi)
位運算 比 常見的取模速度快
(n-1) & hash也就說明了真正有作用的是hash值的后幾位數(shù)字怠缸,而hash的值hashcode ^ (hashcode >> 16) 低16位和高16位做異或運算得到的,意味著hash后幾位數(shù)字由hashcode高低位共同決定扳炬,這也進(jìn)一步的降低hash碰撞

有什么缺點么罐呼?

在JDK1.7中在高并發(fā)場景下擴(kuò)容機(jī)制會出現(xiàn)鏈表循環(huán)的情況(主要是頭插法導(dǎo)致),使得CPU負(fù)載達(dá)到100%厌杜,不過在JDK1.8中已經(jīng)不存在該問題了
線程不安全,無鎖控制瞧壮,也不能控制活躍性問題咆槽,也無法解決發(fā)布安全的問題圈纺,那么該如何解決呢?
擴(kuò)容存在潛在的OOM的情況灯谣,例如當(dāng)前1G的空間已經(jīng)使用了800M蛔琅,再經(jīng)過擴(kuò)容之后就會出現(xiàn)OOM的場景,這樣進(jìn)一步說明了必須預(yù)估業(yè)務(wù)場景的容量大小辜窑,并一次性賦值到位穆碎,減少無所謂的擴(kuò)容操作

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末惨远,一起剝皮案震驚了整個濱河市话肖,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌贺氓,老刑警劉巖床蜘,帶你破解...
    沈念sama閱讀 222,183評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件邢锯,死亡現(xiàn)場離奇詭異,居然都是意外死亡尾抑,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評論 3 399
  • 文/潘曉璐 我一進(jìn)店門榜苫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來翎冲,“玉大人抗悍,你說我怎么就攤上這事÷咛剩” “怎么了疟暖?”我有些...
    開封第一講書人閱讀 168,766評論 0 361
  • 文/不壞的土叔 我叫張陵俐巴,是天一觀的道長欣舵。 經(jīng)常有香客問我缀磕,道長,這世上最難降的妖魔是什么糟把? 我笑而不...
    開封第一講書人閱讀 59,854評論 1 299
  • 正文 為了忘掉前任遣疯,我火速辦了婚禮凿傅,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘辨液。我一直安慰自己箱残,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 68,871評論 6 398
  • 文/花漫 我一把揭開白布赏殃。 她就那樣靜靜地躺著间涵,像睡著了一般。 火紅的嫁衣襯著肌膚如雪抗蠢。 梳的紋絲不亂的頭發(fā)上思劳,一...
    開封第一講書人閱讀 52,457評論 1 311
  • 那天潜叛,我揣著相機(jī)與錄音,去河邊找鬼销斟。 笑死椒舵,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的犁钟。 我是一名探鬼主播涝动,決...
    沈念sama閱讀 40,999評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼侥加,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了昔穴?” 一聲冷哼從身側(cè)響起提前,我...
    開封第一講書人閱讀 39,914評論 0 277
  • 序言:老撾萬榮一對情侶失蹤狈网,失蹤者是張志新(化名)和其女友劉穎笨腥,沒想到半個月后脖母,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體闲孤,經(jīng)...
    沈念sama閱讀 46,465評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡讼积,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,543評論 3 342
  • 正文 我和宋清朗相戀三年勤众,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片吕朵。...
    茶點故事閱讀 40,675評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡窥突,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情音半,我是刑警寧澤曹鸠,帶...
    沈念sama閱讀 36,354評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站坛善,受9級特大地震影響邻眷,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜改衩,卻給世界環(huán)境...
    茶點故事閱讀 42,029評論 3 335
  • 文/蒙蒙 一葫督、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧橄镜,春花似錦、人聲如沸晒夹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽响逢。三九已至棕孙,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間钦铺,已是汗流浹背肢预。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評論 1 274
  • 我被黑心中介騙來泰國打工烫映, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人锭沟。 一個月前我還...
    沈念sama閱讀 49,091評論 3 378
  • 正文 我出身青樓族淮,卻偏偏與公主長得像,于是被迫代替她去往敵國和親贴妻。 傳聞我的和親對象是個殘疾皇子蝙斜,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,685評論 2 360

推薦閱讀更多精彩內(nèi)容