HashMap的相關問題(長期更新)

HashMap的相關問題

解決哈希衝突的一些方法

  • 開放定址法:
    • 開放定址法就是一旦發生了衝突,就去尋找下一個空的散列地址,只要散列表足夠大,空的散列地址總能找到,並將記錄存入。
  • 鏈地址法
    • 將哈希表的每個單元作爲鏈表的頭結點,所有哈希地址爲i的元素構成一個同義詞鏈表。即發生衝突時就把該關鍵字鏈在以該單元爲頭結點的鏈表的尾部。
  • 再哈希法
    • 當哈希地址發生衝突用其他的函數計算另一個哈希函數地址,直到衝突不再產生爲止。
  • 建立公共溢出區
    • 將哈希表分爲基本表和溢出表兩部分,發生衝突的元素都放入溢出表中。

HashMap的簡單介紹

HashMap的底層採用的是數組+鏈表的形式。

簡單來說,數組就是hash桶,然後每一個數組都存放一個鏈表。

然後在jdk1.8中進行了底層的優化,如果hash桶的數量大於64且鏈表長度大於8,那麼就會將鏈表進行樹化,構成一個紅黑樹,將查詢的複雜度從O(n)將爲了O(lgn)。

爲什麼不將鏈表全部換成紅黑樹

很明顯,紅黑樹從時間複雜度上是要優於鏈表的,但是爲什麼沒有全部替換成紅黑樹,原因大概有一下幾點:

  1. 鏈表的結構簡單,紅黑樹的結構複雜
  2. 在小範圍的數據上,紅黑樹的複雜度並沒有優於鏈表很多
  3. 在HashMap擴容的時候,會對紅黑樹進行拆分和重組,這其中的操作比較耗時。

爲什麼長度一定是2的冪次

在jdk1.7中,計算某個哈希值對應的數組索引採用原理是取模。

舉個例子:如果當前計算出的哈希值是9,哈希桶的數量是8,那麼9對應的數組下標就是9%8=1

但是,取模的運算速度是很慢的。因此有了一種快速計算索引的方法。那就是9&(8-1)=1。(位運算)

hash%length = hash&(length-1)

就是用位運算代替取模,但是這種方法只有在模數是2的冪的時候才起作用,所以要求容量大小是2的冪次,就是爲了加快速度,提高性能。同時在另外一方面,也可以減少衝突的可能性。

HashMap中的一些類常量介紹

 //默認hash桶初始長度16
  static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 

  //hash表最大容量2的30次冪
  static final int MAXIMUM_CAPACITY = 1 << 30;

  //默認負載因子 0.75,主要是用於計算閾值
  static final float DEFAULT_LOAD_FACTOR = 0.75f;

  //鏈表的數量大於等於8個並且桶的數量大於等於64時鏈表樹化 
  static final int TREEIFY_THRESHOLD = 8;

  //hash表某個節點鏈表的數量小於等於6時樹拆分
  static final int UNTREEIFY_THRESHOLD = 6;

  //樹化時最小桶的數量
  static final int MIN_TREEIFY_CAPACITY = 64;

代碼問題,關於哈希算法

//通過移位和異或運算,可以讓 hash 變得更復雜,進而影響 hash 的分佈性。
/*
JDK 1.8 中,是通過 hashCode() 的高 16 位異或低 16 位實現的:(h = k.hashCode()) ^ (h >>> 16),主要是從速度,功效和質量來考慮的,減少系統的開銷,也不會造成因爲高位沒有參與下標的計算,從而引起的碰撞。
*/
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

爲什麼要用異或運算符

保證了對象的 hashCode 的 32 位值只要有一位發生改變,整個 hash() 返回值就會改變。儘可能的減少碰撞。

關於comparableClass

For的作用解釋

參考https://blog.csdn.net/weixin_42340670/article/details/80673127

/**
* 如果對象x的類是C,如果C實現了Comparable<C>接口,那麼返回C,否則返回null
*/
static Class<?> comparableClassFor(Object x) {
    if (x instanceof Comparable) {
        Class<?> c; Type[] ts, as; Type t; ParameterizedType p;
        if ((c = x.getClass()) == String.class) // 如果x是個字符串對象
            return c; // 返回String.class
        /*
         * 爲什麼如果x是個字符串就直接返回c了呢 ? 因爲String  實現了 Comparable 接口,可參考如下String類的定義
         * public final class String implements java.io.Serializable, Comparable<String>, CharSequence
         */ 
 
        // 如果 c 不是字符串類,獲取c直接實現的接口(如果是泛型接口則附帶泛型信息)    
        if ((ts = c.getGenericInterfaces()) != null) {
            for (int i = 0; i < ts.length; ++i) { // 遍歷接口數組
                // 如果當前接口t是個泛型接口 
                // 如果該泛型接口t的原始類型p 是 Comparable 接口
                // 如果該Comparable接口p只定義了一個泛型參數
                // 如果這一個泛型參數的類型就是c,那麼返回c
                if (((t = ts[i]) instanceof ParameterizedType) &&
                    ((p = (ParameterizedType)t).getRawType() ==
                        Comparable.class) &&
                    (as = p.getActualTypeArguments()) != null &&
                    as.length == 1 && as[0] == c) // type arg is c
                    return c;
            }
            // 上面for循環的目的就是爲了看看x的class是否 implements  Comparable<x的class>
        }
    }
    return null; // 如果c並沒有實現 Comparable<c> 那麼返回空
}

關於loadFactor 負載因子大小的調整

在源碼種,loadFactor 的默認值是0.75,但是有一個構造器也提供了修改loadFactor的。但是源碼中並沒有對loadFactor 進行任何的限制,也就是說允許很小,也允許很大。

那麼這就可以牽扯到不同的場景,如果loadFactor 很小,那麼閾值就會很小,也就是稍微添加幾個元素就會觸發擴容機制,將容量和閾值擴大兩倍,這麼做帶來的好處在於減小哈希衝突的可能性,但是增大了空間的消耗,是典型的空間換時間。

那麼loadFactor 也可以大於1,那麼這時候閾值就會大於cap,也就是可以理解就算是把每一個哈希桶裝滿,也還是能夠容納元素,而不觸發擴容機制。也就是典型的時間換空間的做法。

tableSizeFor方法解析

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

這個方法的作用就是在初始化HashMap的時候,因爲要求容量是2的冪次,但是如果你傳入的不是一個2的冪次的數,代碼底層就會把你傳入的那個數字優化爲一個2的冪次。

首先是n = cap-1,減一的目的是如果傳入的cap恰好爲2的冪次,那麼通過下面的算法的時候會多擴大一倍,模擬一下就知道效果了。

下面操作的原理大概是這樣的,對於一個數,它的二進制最高爲爲1,那麼將這個數右移一位再或起來,那麼高兩位就一定爲1。然後將新得到的數或上右移兩位,那麼高4爲就一定爲1。以此類推。

最後得到的就是一個2進制上全爲1的數了,最後再加1,那麼返回的就是一個2的冪次數。

至於爲什麼沒有 n |= n >>> 32,那是因爲容量最大是2的32次。

初始化threshold的地方

  1. 構造器傳入一個初始容量的時候,會設置閾值this.threshold = tableSizeFor(initialCapacity);
  2. 構造器傳入一個Map的時候,會設置閾值threshold = tableSizeFor(t);

核心方法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;
    //這就是所謂的延遲創建,在第一次put元素的時候,初始化底層數組
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    //如果當前元素對應的數組位置是空的,那麼就直接把對應的Node節點加進去
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        //發現數組的對應位置有值,但是發現要put的鍵值對的key和當前位置的相同
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        //當前節點是紅黑樹節點了,那就要調用添加紅黑樹節點的方法
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            //不然就遍歷整個鏈表,看有沒有和要put的鍵值對一樣的key
            for (int binCount = 0; ; ++binCount) {
                //發現鏈表到底了還沒有找到一樣的key,那就直接添加新的鏈表節點元素
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    //發現鏈表長度到達閾值了,嘗試樹化鏈表
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                //鏈表中找到了一樣的key
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        //如果上面找到了一的key,那就用傳進來的value值更新value屬性
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    //長度到達了閾值,擴容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

核心方法resize

首先有一點,1.8中,把初始化的操作延遲到了第一次put的時候。

這個resize方法的作用很多,不僅在每個HashMap首次添加元素的時候會調用,而且在容量超過閾值的時候,都會調用,基本是每次擴大2倍。

說明一下構造器

便於解釋一下下方的註釋

總共有4種,但是大體可以分爲3類

public HashMap(int initialCapacity, float loadFactor) {}
public HashMap(int initialCapacity) {}
public HashMap() {}
public HashMap(Map<? extends K, ? extends V> m) {}

第一類:帶初始容量的,設置了閾值

第二類:無參構造器,什麼都沒有

第三類:傳入集合,既設置閾值,同時也會調用resize()

e.hash & oldCap

這個表達式的作用是這樣的,我們知道oldCap一定是一個2的冪次,所以它的二進制上只有一個1,也就是形如000010000,這種形式。所以e.hash & oldCap這個表達式的結果只可能有2種,要麼是0,要麼是oldCap,而且從概率上來說,兩者的概率應該是五五開的。那麼這樣子就就可以用於下方鏈表的分裂操作了,也即是把一個鏈表拆分成兩個鏈表,拆分的依據就是這兩種不同的結果。

還有一點,關於怎麼擴容

由於容器是2的冪次的,擴容也是每次擴大兩倍,那麼就會有一個很好的特點。

假設舊容器大小時oldcap,新容器newcap的大小爲newcap=oldcap*2

那麼,對於old數組下標爲index上的內容,擴容到新的數組上就只有2種取值,要麼是原位置,要麼是原位置+oldcap

舉個例子:

如果之前的oldcap是4,那麼原來哈希值爲1 % 4 = 1,5 %4 = 1,9 % 4 = 1,13 % 4 = 1的就都會映射到index爲1的位置。

現在經過擴容之後,newcap = 8,那麼1 % 8 = 1,5 % 8 = 5,9 %8 = 1,13 % 8 = 5,結果就很明顯了。

關於增加鏈表長度的三步

  1. 首先要記錄起始節點和尾節點

  2. 每當增加一個元素,那麼就要更新尾節點.next 屬性,同時要將尾節點往後移

  3. 所有元素添加完畢之後,將尾節點的next屬性置爲null,不然就死循環了。

源碼註釋

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    //不是初始化的調用,因爲有值
    if (oldCap > 0) {
  		  //這個判斷是在容量到達上限的時候
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        //正常的2倍擴容
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; 
    }
    //初始化調用,設置新容量爲就閾值
    else if (oldThr > 0) 
        newCap = oldThr;
   //初始化調用,且沒有設置新閾值,就是無參構造器
    else {           
        //新容量就是默認的16
        newCap = DEFAULT_INITIAL_CAPACITY;
        //新閾就是16*0.75=12
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    //這個就是對另外兩類構造器的完善,設置新閾值
    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;
    //下面都是針對擴容而言,而不是初始化
    if (oldTab != null) {
        //遍歷舊數組
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            //舊數組對應的索引位置有值
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                //如果是單個的,直接賦值
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                //如果是紅黑樹的節點,那麼就進行樹的分割,下面會詳細講述
                else if (e instanceof 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;
                    //loHead和loTail可以理解爲第一個鏈表的首節點和尾節點
                    //hiHead和hiTail可以理解爲第二個鏈表的首節點和尾節點
                    do {
                        next = e.next;
                        //這就是上面所說的,根據表達式的值,分兩類
                        if ((e.hash & oldCap) == 0) {
                            //第一個鏈表記錄首節點
                            if (loTail == null)
                                loHead = e;
                            else
                                //相當於鏈表長度+1
                                loTail.next = e;
                            //更新尾節點
                            loTail = e;
                        }
                        //下面和上面同理
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    //將第一個鏈表接到新數組的j位置
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    //將第二個鏈表接到新數組的j+oldcap位置
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

treeifyBin(樹化)的部分細節

如何判斷節點在紅黑樹的左邊還是右邊

  1. 先判斷hash值,小的往左,大的往右,相等就看第二步
  2. 判斷能不能通過comparable接口去判斷,如果不能或者還是相等,就第三步
  3. 調用tieBreakOrder方法,最後一定能夠區分是往左還是往右

爲什麼判斷左右是根據哈希值//

源碼分析

final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    //必須滿足鏈表長度大於8(進入該函數已滿足),並且哈希桶的數量大於64才能進行樹化操作
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    //當前對應下標的數組中有值
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        TreeNode<K,V> hd = null, tl = null;
        do {
            //將鏈表節點準換爲樹節點,其實是一個建立雙向鏈表的過程
            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)
            //真正的樹化
            hd.treeify(tab);
    }
}
final void treeify(Node<K,V>[] tab) {
    TreeNode<K,V> root = null;
    //遍歷雙向鏈表
    for (TreeNode<K,V> x = this, next; x != null; x = next) {
        next = (TreeNode<K,V>)x.next;
        x.left = x.right = null;//初始化左右兒子
        //設置根節點
        if (root == null) {
            x.parent = null;
            x.red = false;
            root = x;
        }
        else {
            K k = x.key;
            int h = x.hash;
            Class<?> kc = null;
            //遍歷已經存在的紅黑樹的結構,判斷當前的節點應該插在哪裏
            for (TreeNode<K,V> p = root;;) {
                int dir, ph;
                K pk = p.key;
                if ((ph = p.hash) > h)
                    dir = -1;
                else if (ph < h)
                    dir = 1;
                else if ((kc == null &&
                          (kc = comparableClassFor(k)) == null) ||
                         (dir = compareComparables(kc, k, pk)) == 0)
                    dir = tieBreakOrder(k, pk);
				//dir的過程標題開頭已經介紹過了
                TreeNode<K,V> xp = p;
                //插入節點
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
                    x.parent = xp;
                    if (dir <= 0)
                        xp.left = x;
                    else
                        xp.right = x;
                    root = balanceInsertion(root, x);
                    break;
                }
            }
        }
    }
    /*
    這個方法裏做的事情,就是保證樹的根節點一定也要成爲鏈表的首節點
    這是爲了紅黑樹和鏈表互相轉化方便,下面就不分析這個方法的源碼了	
    */
    moveRootToFront(tab, root);
}

可以看一下源碼中紅黑樹平衡是怎麼寫的

關於紅黑樹的概念以及理解,可以看我的這篇博客

static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
                                           TreeNode<K,V> x) {
    //插入的節點默認是紅色
    x.red = true;
    /*
    	關於這些變量:
    	xp:x節點的父親節點
    	xpp:xp節點的父親節點,也就是x節點的祖父節點
    	xppl:x的祖父節點的左兒子
    	xppr:x的祖父節點的右兒子
    */
    for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
		//發現是根節點,設爲黑色,返回        
        if ((xp = x.parent) == null) {
            x.red = false;
            return x;
        }
        //如果父親節點是黑色或者祖父爲空,其實也就是父親節點是根節點
        else if (!xp.red || (xpp = xp.parent) == null)
            return root;
        //x的父親節點是x的祖父節點的左兒子,當然了x的父節點是紅色的
        if (xp == (xppl = xpp.left)) {
            //x的叔叔節點存在並且是紅色
            if ((xppr = xpp.right) != null && xppr.red) {
                xppr.red = false;
                xp.red = false;
                xpp.red = true;
                x = xpp;
            }
            //x的叔叔節點不存在或者是爲黑色
            else {
                //x是x的父節點的右兒子
                if (x == xp.right) {
                    //左旋,同時修改x節點爲x的父節點(x=xp)
                    root = rotateLeft(root, x = xp);
                    //不知道爲什麼要再次確認xpp,在我的認知裏,應該不會修改xpp的值
                    xpp = (xp = x.parent) == null ? null : xp.parent;
                }
                //修改xp
                if (xp != null) {
                    xp.red = false;
                    if (xpp != null) {
                        //修改xpp
                        xpp.red = true;
                        //對xpp進行右旋
                        root = rotateRight(root, xpp);
                    }
                }
            }
        }
        //下面和上面是差不多的
        else {
            if (xppl != null && xppl.red) {
                xppl.red = false;
                xp.red = false;
                xpp.red = true;
                x = xpp;
            }
            else {
                if (x == xp.left) {
                    root = rotateRight(root, x = xp);
                    xpp = (xp = x.parent) == null ? null : xp.parent;
                }
                if (xp != null) {
                    xp.red = false;
                    if (xpp != null) {
                        xpp.red = true;
                        root = rotateLeft(root, xpp);
                    }
                }
            }
        }
    }
}

removeNode方法分析

/*
matchValue 如果爲true,則當key對應的鍵值對的值equals(value)爲true時才刪除;否則不關心value的值
movable 刪除後是否移動節點,如果爲false,則不移動
*/
final Node<K,V> removeNode(int hash, Object key, Object value,
                           boolean matchValue, boolean movable) {
    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;
       //node節點的作用就是獲得相匹配的要刪除的節點
           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 {
                //遍歷鏈表,找到一個相匹配的節點,p的作用是記錄要刪除節點的前驅節點
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
           //紅黑樹節點刪除
            if (node instanceof TreeNode)
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
           //要刪除的節點是鏈表頭
            else if (node == p)
                tab[index] = node.next;
            //刪除的節點是鏈表中間的節點,用下面的表達式表示刪除
            else
                p.next = node.next;
            ++modCount;
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

內部迭代器的分析

//這是抽象類,內部沒有實現Next方法,因爲HashMap的迭代器可以有多種,遍歷key的,value的,entry的,所以交給子類實現
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;
        Node<K,V>[] t = table;
        current = next = null;
        index = 0;
        //關鍵代碼,因爲底層數組不一定每個下標都有內容,所以通過遍歷找到第一個有值的地方
        if (t != null && size > 0) { // advance to first entry
            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;
        Node<K,V> e = next;
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        if (e == null)
            throw new NoSuchElementException();
        //要麼找到鏈表的下一個位置,要麼是找到下一個數組index,這個是不會用到紅黑樹節點的,因爲沒必要
        if ((next = (current = e).next) == null && (t = table) != null) {
            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);
        expectedModCount = modCount;
    }
}
//三種具體的實現類,分別迭代key,value,entry
final class KeyIterator extends HashIterator
    implements Iterator<K> {
    public final K next() { return nextNode().key; }
}

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

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

紅黑樹什麼時候退化

鏈表長度小與6的時候

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章