HashMap源碼學習

經常使用方法

put方法

描述:

最經常使用的方法之一,用來向hash桶中添加 鍵值對.可是這個方法並不會去執行實際操做.而是委託putVal方法進行處理java

代碼:

public V put(K key, V value) {
    // 此次個調用分別指定了hash,key,value,替換現有值,非建立模式
    return putVal(hash(key), key, value, false, true);
}

這裏調用了hash方法獲取了keyhash,後面單獨說這個hash的意義node

putVal方法

描述:

實際執行put操做的方法.算法

代碼:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    // tab - 當前hash桶的引用
    // p - key所表明的節點(此節點不必定是目標節點,而僅僅是hash與桶長度的計算值相同而已)(它不爲空時多是鏈表或紅黑樹)
    // n - 當前桶的容量
    // i - key在桶中的下標(同p,不表明目標節點)
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 初始化局部變量tab並判斷是否爲空,初始化局部變量n並判斷是否爲0
    // PS: 源碼中大量的使用了這種書寫方法,不知道放在某寫大廠裏會怎麼樣(斜眼笑)
    if ((tab = table) == null || (n = tab.length) == 0)
        // 當tab爲空或n爲0時,代表hash桶還沒有初始化,調用resize()方法,進行初始化並再次初始化局部變量tab與n
        n = (tab = resize()).length;
    
    // 初始化p與i
    // 這裏使用了(n - 1) & hash的方式計算key在桶中的下標.這個在後面單獨說明
    // 當p是否爲空
    if ((p = tab[i = (n - 1) & hash]) == null)
        // p爲空,調用newNode方法初始化節點並賦值到tab對應下標
        tab[i] = newNode(hash, key, value, null);
    else {
        // p不爲空,發生碰撞.進行後續處理
        
        // e - 目標節點
        // k - 目標節點的key
        Node<K,V> e; K k;
        
        // 判斷key是否相同.(這裏除了比較key之外,還比較了hash)
        // 注意,這裏同時初始化了局部變量k,可是在第二組條件不知足的狀況下,沒有使用價值,能夠被忽略
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            // key相同,將e(目標節點)設置爲p
            e = p;
        // 判斷節點是不是紅黑樹
        else if (p instanceof TreeNode)
            // 肯定時,直接委派處理
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            // 走到這裏,表明當前節點爲普通鏈表,進行遍歷查找
            // 變量binCount只做爲是否達到tree化的閾值判斷條件.
            for (int binCount = 0; ; ++binCount) {
                
                // 獲取鏈表的下一個元素,並賦值到e(此時e是一箇中間變量,不肯定是不是目標節點)
                // 第一次for循環時,p表明hash桶中的節點(同時也是鏈表的頭部節點),以後一直等於p.next
                if ((e = p.next) == null) {
                    // 鏈表遍歷到末尾
                    
                    // 向鏈表中追加新的節點
                    p.next = newNode(hash, key, value, null);
                    
                    // 判斷當前鏈表長度是否達到tree閾值
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        // 調用treeifyBin方法直接處理
                        treeifyBin(tab, hash);
                    // 中斷循環
                    // 注意,此時局部變量e=null
                    break;
                }
                
                // 能走到此處,說明鏈表未結束,比較e的k是否相同(hash與==)
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    // key相同
                    break;
                
                // e既不爲null也不是目標節點,賦值到p,準備進行下次循環
                p = e;
            }
        }
        
        // 判斷e是否存在
        if (e != null) { // existing mapping for key
            // e不等於null說明操做爲"替換"
            
            // 緩存老值
            V oldValue = e.value;
            // 判斷是否必須替換或老值爲null
            if (!onlyIfAbsent || oldValue == null)
                // 必須替換或老值爲空,更新節點e的value
                e.value = value;
            // 調用回調
            afterNodeAccess(e);
            // 返回老值
            // 注意,這裏直接返回了,而沒有進行modCount更新與下面的後續操做
            return oldValue;
        }
    }
    
    // 除了更新鏈表節點之外,都會走到這裏(putTreeVal的返回值是什麼有待確認)
    // modCount+1
    ++modCount;
    // size+1(元素數量+1)
    // 判斷是否超過閾值
    if (++size > threshold)
        // 重置大小
        resize();
    // 調用後置節點插入回調
    afterNodeInsertion(evict);
    return null;
}

resize方法

描述:

用於添加鍵值對後的擴容與對槽從新分佈的操做數組

代碼:

final Node<K,V>[] resize() {
    //----------------------------------- 新容量與閾值計算 -----------------------------------
    
    // 緩存桶引用
    Node<K,V>[] oldTab = table;
    // 緩存老的桶的長度,桶爲null時,使用0
    // 注意,這裏用的是oldTab.length,而不是size
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    // 緩存閾值
    int oldThr = threshold;
    // 新桶容量與閾值
    int newCap, newThr = 0;
    
    // 老容量大於.這通常表明這個桶已經通過了resize的數次處理
    if (oldCap > 0) {
        
        // 老容量大於MAXIMUM_CAPACITY = 1 << 30 = 1073741824
        // 容量計算方式爲n<<1,當oldCap >= MAXIMUM_CAPACITY時,再次執行位移.其可能的最大值就是Integer.MAX_VALUE
        if (oldCap >= MAXIMUM_CAPACITY) {
            // 設置閾值爲Integer.MAX_VALUE
            threshold = Integer.MAX_VALUE;
            // 直接return.放棄所有後續處理
            return oldTab;
        }
        // 使用oldCap << 1初始化newCap
        // 當oldCap小於MAXIMUM_CAPACITY而且oldCap大於DEFAULT_INITIAL_CAPACITY(16)時
        // 此時newCap可能已經大於MAXIMUM_CAPACITY而且newThr=0或者newCap很小(小於16>>2)而且newThr=0
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            //設置newThr爲oldThr << 1(這裏沒有作正確性校驗,待查)
            newThr = oldThr << 1; // double threshold
    }
    // 判斷老閾值是否大於0
    // 走到這說明oldCap==0,而且使用了包含initialCapacity參數的構造器構造了這個map,且沒有被添加過元素
    else if (oldThr > 0) // initial capacity was placed in threshold
        // 使用將新容量複製爲老閾值(newCap此時爲0)
        // 注意: 在使用了包含initialCapacity參數的構造方法時,其threshold已經被計算爲2的n次冪
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        // 默認方法,當使用無參構造方法時,會出現oldThr與oldCap都等於0的狀況
        // 使用默認初始化容量賦值到newCap
        newCap = DEFAULT_INITIAL_CAPACITY;
        // 使用默認初始化容量與加載因子相乘賦值到newThr
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    
    // 統一處理newThr
    if (newThr == 0) {
        // 新容量與加載因子相乘
        float ft = (float)newCap * loadFactor;
        // 當newCap與ft均小於MAXIMUM_CAPACITY時,newThr=ft.不然newThr=Integer.MAX_VALUE
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    
    //----------------------------------- 元素重排 -----------------------------------
    // 更新threshold
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    // 更新桶對象(此時是空的)
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    // 判斷老桶是否爲空
    if (oldTab != null) {
        // 老桶不爲空.進行遍歷
        for (int j = 0; j < oldCap; ++j) {
            // 桶元素
            Node<K,V> e;
            // 進行桶元素獲取
            // 判斷桶元素是否存在(由於使用(n-1)&hash的方式進行計算,因此常常會出現這種狀況)
            if ((e = oldTab[j]) != null) {
                // 刪除引用
                oldTab[j] = null;
                // 判斷桶元素是否有下一個元素
                if (e.next == null)
                    // 沒有下一個元需.使用相同的算法計算在新桶中的下標並賦值
                    newTab[e.hash & (newCap - 1)] = e;
                // 桶元素存在next,判斷是否爲TreeNode
                else if (e instanceof TreeNode)
                    // 進行委派執行
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    // 對於鏈表結構,拆分到高位與低位兩組
                    
                    // loHead與loTail非別表明低位頭與低位尾
                    Node<K,V> loHead = null, loTail = null;
                    // hiHead與hiTail非別表明高位頭與高位尾
                    Node<K,V> hiHead = null, hiTail = null;
                    // next
                    Node<K,V> next;
                    // 已經存在遍歷目標,直接使用do while
                    do {
                        // 拿到e的next.
                        next = e.next;
                        // 判斷e的hash是不是高位
                        // 判斷原理以下.
                        // 首先oldCap恆定爲2的n次冪,二進制表達爲1000...
                        // 下標計算方程爲(n-1)&hash
                        // 帶入n後,爲...111&hash
                        // 當n=111時,hash爲1101,結果爲101
                        // 當n=1111時,hash爲1101,結果爲1101.表示爲高位(注意hash的高位)
                        // 當n=1111時,hash爲0101,結果爲101.表示爲低位(注意hash的高位)
                        // 這樣就,能夠直接求出新的下標.可是,這種方式須要對全部的元素進行從新計算,很是低效
                        // 因此jdk使用了一個特別的方法.就是直接比較最高位,當一個hash與數組長度(也就是n的n次冪)時,如1101&1000
                        // 當結果等於0時,表明這個hash是低位hash,其餘就是高位hash
                        if ((e.hash & oldCap) == 0) {
                            // 低位
                            // 判斷低位尾部是否存在
                            if (loTail == null)
                                // 不存在,表明頭部也沒有,進行初始化
                                loHead = e;
                            else
                                // 存在,追加到尾部的next
                                loTail.next = e;
                            // 更新尾部
                            loTail = e;
                        }
                        else {
                            // 高位
                            if (hiTail == null)
                                // 不存在,表明頭部也沒有,進行初始化
                                hiHead = e;
                            else
                                // 存在,追加到尾部的next
                                hiTail.next = e;
                            // 更新尾部
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    
                    // 進行收尾處理
                    // 判斷低位是否爲空
                    if (loTail != null) {
                        // 不爲空
                        // 清除末尾元素的next.當loTail是鏈表倒數第二個元素且倒數第一個元素是高位元素時,須要清空loTail的next對高位元素的引用
                        loTail.next = null;
                        // 低位使用原下標進行保存
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        // 不爲空
                        // 清除末尾元素的next.理由同上但判斷方式相反
                        hiTail.next = null;
                        // 低位使用原下標+原容量進行保存
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    // 返回newTab
    return newTab;
}

treeifyBin方法

描述:

當某槽內的鏈表長度大於閾值後,此方法會被調用.將槽內對應位置的鏈表替換爲紅黑樹.
注意:這個方法內只是將鏈表替換成了紅黑樹對象TreeNode.此時仍是鏈狀結構,沒有組裝成紅黑樹的結構.須要在最後帶用鏈表頭部對象的TreeNode.treeify方法完成樹化緩存

代碼:

final void treeifyBin(Node<K,V>[] tab, int hash) {
    // n - 表明參數tab長度
    // index - tab中表示hash的下標
    // hash - 待處理的鏈表節點hash
    // e - 目標節點
    int n, index; Node<K,V> e;
    // 判斷tab是否爲空或tab長度MIN_TREEIFY_CAPACITY=64
    // 也就是說,在桶中單個鏈表長度可能已經達到要求(如putVal中的binCount >= TREEIFY_THRESHOLD - 1),可是桶容量未達標時,也不會進行tree化
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        // 表是空的或表容量小於MIN_TREEIFY_CAPACITY
        // 重置大小
        resize();
    // 能夠tree化,檢查鏈表節點是否存在
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        // 鏈表節點存在
        
        // 樹節點頭與尾
        TreeNode<K,V> hd = null, tl = null;
        // 已經有第一個目標,直接do while
        do {
            // 構造一個TreeNode.(這裏沒有額外邏輯,僅僅是使用當前的e建立了TreeNode)
            // 注意,這裏的Tree繼承自LinkedHashMap.Entry,內部包含了before與after的雙向鏈表.可是TreeNode又自行實現了雙向鏈表prev與next,並無使用前者的數據結構
            TreeNode<K,V> p = replacementTreeNode(e, null);
            // 判斷尾部是否爲空
            if (tl == null)
                // 初始化頭部
                hd = p;
            else {
                // 尾部不爲空
                // 設置上一個節點
                // 設置尾部下一個節點
                p.prev = tl;
                tl.next = p;
            }
            // 交換尾部
            tl = p;
        } while ((e = e.next) != null);
        
        // 賦值並判斷頭部節點是否爲空
        if ((tab[index] = hd) != null)
            // 調用TreeNode的treeify組裝紅黑樹
            hd.treeify(tab);
    }
}

即便被調用,這個方法也不保證替換對應槽內的鏈表到紅黑樹.這還須要檢查當前桶的容量是否達到閾值MIN_TREEIFY_CAPACITY數據結構

TreeNode.treeify方法

描述:

將鏈表結構的數據轉換成紅黑樹結構的數據的實際執行者(此時鏈表中的全部對象已是TreeNode類型的了)app

代碼:

final void treeify(Node<K,V>[] tab) {
    // 根節點(黑色節點)
    TreeNode<K,V> root = null;
    // 進行迭代.(當前this做用域位於TreeNode實例)
    // x表示當前遍歷中的節點
    for (TreeNode<K,V> x = this, next; x != null; x = next) {
        // 緩存next
        next = (TreeNode<K,V>)x.next;
        // 保證當前節點左右節點爲null
        x.left = x.right = null;
        // 判斷是否存在根節點
        if (root == null) {
            // 不存在
            // 跟節點沒有父級.因此設置爲null
            x.parent = null;
            // 紅黑樹中,根節點是黑色的
            x.red = false;
            // 保存到局部變量
            root = x;
        }
        else {
            // 跟節點已確認
            
            // 緩存key
            K k = x.key;
            // 緩存hash
            int h = x.hash;
            // key類型
            Class<?> kc = null;
            // -------------------- 對跟節點進行遍歷,查找插入位置 --------------------
            // p是插入節點的父節點
            for (TreeNode<K,V> p = root;;) {
                // dir - 用來表達左右.
                // ph - 父節點hash
                int dir, ph;
                // 父節點key
                K pk = p.key;
                
                // -------------------- 判斷插入到左仍是右節點 --------------------
                // 初始化父節點hash
                // 判斷父節點hash是否大於當前節點hash
                if ((ph = p.hash) > h)
                    // dir = -1 插入節點在父節點左側
                    dir = -1;
                // 判斷父節點hash是否小於當前節點hash
                else if (ph < h)
                    // dir = 1 插入節點在父節點右側
                    dir = 1;
                // 父節點hash等於當前節點hash,進行額外的處理
                // 這裏使用了基於Class的一些處理辦法,最終保證了dir的正確值(不爲0) TODO 待補充 
                else if ((kc == null &&
                          (kc = comparableClassFor(k)) == null) ||
                         (dir = compareComparables(kc, k, pk)) == 0)
                    dir = tieBreakOrder(k, pk);


                // -------------------- 獲取左或右節點並進行操做 --------------------
                // 緩存插入節點的父節點
                TreeNode<K,V> xp = p;
                // 使用dir獲取父節點對應的左或右節點,而且檢查這個節點是否爲null.不爲null時,進入下一次循環
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
                    // 父節點左或右節點爲null
                    
                    // 設置父級節點
                    x.parent = xp;
                    // 再次判斷左右
                    if (dir <= 0)
                        // 將父節點的左子節點複製爲當前節點
                        xp.left = x;
                    else
                        // 將父節點的右子節點複製爲當前節點
                        xp.right = x;
                    // 進行平衡
                    root = balanceInsertion(root, x);
                    // 退出查找插入位置的循環,進行下一個元素的插入
                    break;
                }
            }
        }
    }
    
    // 由於在進行旋轉操做時,可能會修改根節點到其餘節點.致使桶中的直接節點爲分支節點,因此須要進行修正
    moveRootToFront(tab, root);
}

hash方法

描述:

HashMap本身使用的hash的計算方式.做爲key比較,index的計算依據.它經過對象原hash與其高16位進行^運算,而得出一個新值作回hash.性能

代碼:

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

這裏之因此沒有直接使用key的hash.是爲了應對當key的hash分佈很是差的時候,會間接致使
hash桶的分佈很是差,從而影響性能.因此使用原hash異或(XOR)原hash的高16位,做爲實際使用的hash
這裏之因此使用16:16,而不是8:8:8:8或其餘值,是由於jdk開發者充分考慮了時間,效率,性能等各方面
的狀況後的折中選擇.
同時也是由於當前jdk大多數的hash已經有了較好的分佈,因此也不須要進行過多的處理
計算過程以下
10000000000000000000000000000000
00000000000000001000000000000000
10000000000000001000000000000000this

tableSizeFor方法

描述:

通常用做threshold的初始化工做.他會返回一個大於輸入值的最小的2的冪.已是2的冪時會再次返回它.code

代碼:

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 - 1部分,最後說)
10000 16 n初始狀態
11000 24 n|=n>>>1 等價於 n = 10000|01000 = 11000
11110 30 n|=n>>>2 等價於 n = 11000|00110 = 11110
11111 31 n|=n>>>4 等價於 n = 11110|00001 = 11111
11111 31 略
11111 31 略
100000 32 n+1
最後,經過+1,將...111變爲100...,即2的n次冪

這裏使用了一個頗有意思的方式完成了工做,就是輸入值的最高有效位.
經過不斷的向低位複製最高有效位(1),將全部低位換爲1,最終這個值等於(2^n)-1.同時
也是當前數字的最高位能表達的最大值
那麼,再對這個值+1就可使這個值變成2^n.也就是大於輸入值的最小的2的冪

cap - 1的做用:
若是輸入值已是2的冪,那麼這個方法應該直接返回他.直接進行-1,使用原邏輯便可

內部類

HashIterator

描述:

HashMap本身實現的迭代器.主要用來約束對父類成員的引用.同時實現了remove,nextNode,hasNext等必須方法.爲了方便子類實現,在nextNode方法中直接返回了Node類型對象.用來直接獲取key與value

代碼:

abstract class HashIterator {
    /**
    * 下一個節點 
    */
    Node<K,V> next;        // next entry to return
    /**
    * 當前節點 
    */
    Node<K,V> current;     // current entry
    /**
    * 修改計數
    */
    int expectedModCount;  // for fast-fail
    /**
    * 當前下標(對於父級成員變量table來講,它指向桶中的一個槽(slot))
    */
    int index;             // current slot

    /**
    * 構造方法 
    */
    HashIterator() {
        // 緩存修改計數
        expectedModCount = modCount;
        // 緩存桶 
        Node<K,V>[] t = table;
        // 進行置空(這一步是必須的嗎????) 
        current = next = null;
        // 索引置0(爲啥?????) 
        index = 0;
        // 檢查桶是否已經初始化 
        if (t != null && size > 0) { // advance to first entry
            // 提早獲取並保存next
            do {} while (index < t.length && (next = t[index++]) == null);
        }
    }

    public final boolean hasNext() {
        return next != null;
    }

    final Node<K,V> nextNode() {
        // 指向當前桶
        Node<K,V>[] t;
        // 緩存next.準備替換next.同時e做爲結果返回
        Node<K,V> e = next;
        // fast-fail
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        // next不該爲空
        if (e == null)
            throw new NoSuchElementException();
        // -------------------- 尋找next --------------------
        // 設置current爲e,next爲e.next
        // 判斷next是否爲null
        // 若是爲空,獲取當前桶
        // 判斷桶是否爲空(能走到這裏,說明以前已經在桶中獲取了節點,那桶怎麼回事空的呢?????)
        if ((next = (current = e).next) == null && (t = table) != null) {
            // 上面的next獲取失敗,這裏使用切換槽位的方式尋找下一個next
            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();
        // fast-fail
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        // 刪除當前迭代其中的current
        current = null;
        // 獲取key
        K key = p.key;
        // 調用父級刪除方法
        // 這裏設置了movable爲false,刪除後不移動節點.這個值只對treeNode生效,去要考證設置成false的做用
        // 貌似是在迭代器中被設置了false
        removeNode(hash(key), key, null, false, false);
        // 更新計數
        expectedModCount = modCount;
    }
}

KeyIterator

描述:

HashIterator的實現.包裝了其中的nextNode方法,返回Nodekey

代碼:

final class KeyIterator extends HashIterator
    implements Iterator<K> {
    public final K next() { return nextNode().key; }
}

ValueIterator

描述:

HashIterator的實現.包裝了其中的nextNode方法,返回Nodevalue

代碼:

final class ValueIterator extends HashIterator
    implements Iterator<V> {
    public final V next() { return nextNode().value; }
}

EntryIterator

描述:

HashIterator的實現.包裝了其中的nextNode方法,直接返回了Node

代碼:

final class EntryIterator extends HashIterator
    implements Iterator<Map.Entry<K,V>> {
    public final Map.Entry<K,V> next() { return nextNode(); }
}

KeySet

描述:

繼承了AbstractSet,並經過內部類的特性,使實現方法經過直接調用父類HashMap的引用完成完成

代碼:

final class KeySet extends AbstractSet<K> {
    /**
    * 返回hashMap的成員變量size
    * @return 
    */
    public final int size()                 { return size; }
    /**
    * 由於是同名方法,因此只能使用類名.this.MethodName()的方式調用了
    */
    public final void clear()               { HashMap.this.clear(); }
    /**
    * 返回一個內部類key迭代器
    * @return 
    */
    public final Iterator<K> iterator()     { return new KeyIterator(); }
    /**
    * 調用父類方法
    * @param o
    * @return 
    */
    public final boolean contains(Object o) { return containsKey(o); }
    /**
    * 調用父類方法.標記不須要匹配值,刪除後重建
    * @param key
    * @return 
    */
    public final boolean remove(Object key) {
        return removeNode(hash(key), key, null, false, true) != null;
    }
    /**
    * 返回一個內部類keySpl迭代器
    * @return 
    */
    public final Spliterator<K> spliterator() {
        return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0);
    }
    /**
    * 實現forEach
    * 這裏有個須要注意的地方
    * 對於fail-fast,這個方法會在全部元素迭代完成以後進行,才進行判斷
    * @param action
    */
    public final void forEach(Consumer<? super K> action) {
        Node<K,V>[] tab;
        if (action == null)
            throw new NullPointerException();
        if (size > 0 && (tab = table) != null) {
            int mc = modCount;
            for (int i = 0; i < tab.length; ++i) {
                for (Node<K,V> e = tab[i]; e != null; e = e.next)
                    action.accept(e.key);
            }
            if (modCount != mc)
                throw new ConcurrentModificationException();
        }
    }
}
相關文章
相關標籤/搜索