JDK源碼(容器篇)

JDK容器

前言

閱讀JDK源碼有段時間了,準備以博客的形式記錄下來,也方便複習時查閱,本文參考JDK1.8源碼。java

1、Collection

Collection是全部容器的基類,定義了一些基礎方法。List、Set、Map、Queue等子接口都繼承於它,並根據各自特性添加了額外的方法。node

2、List系列

1.ArrayList

ArrayList是使用頻率較高的容器,實現較爲簡單。內部主要靠一個可自動擴容的對象數組來維持,算法

transient Object[] elementData;

能夠經過構造函數指定數組的初始容量,也能夠不指定,當首次經過add加入元素時,會經過內部擴容機制新建一個容量爲10的數組(JDK1.7前在構造函數中直接新建數組):數組

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}

從上面代碼能夠看出,當容量不夠時,ArrayList每次擴容1.5倍,而後將原對象數組中的元素拷貝至新的對象數組。Arrays.copyOf(Object[],int)方法先是根據傳入的新容量新建數組,而後將元素拷貝到新數組,拷貝操做是經過System.arrayCopy方法來完成的,這是native方法。這兩個方法在容器類中常用,用以拷貝數組。安全

ArrayList支持指定位置的賦值,經過set(int,E)和add(int,E)方法,在執行這些操做時都須要對範圍進行檢查,同理還有獲取和移除指定位置的元素。數據結構

上面發現elementData數組是transient的,這代表系統默認序列化執行時跳過該數組。取而代之的是,ArrayList提供了writeObject和readObject方法來自定義寫入和讀取該數組的方式。併發

最後,做爲容器類的共性,ArrayList實現了Iterable接口,並經過內部類定義了Itr、ListItr這兩個迭代器。spliterator是爲了並行遍歷,會在後面統一分析。app

2.LinkedList

LinkedList其實與Arraylist有不少類似地方,只不過底層實現一個是經過數組,一個是經過鏈表而已。因爲這兩種實現的不一樣,也致使的它們不一樣的使用場合。ArrayList是用數組實現的,那麼在查的方面確定優於基於鏈表的LinkedList,與之相對的是LinkedList在增刪改上優於ArrayList。
LinkedList的核心數據結構以下:函數

transient Node<E> first;
transient Node<E> last;

private static class Node<E> {
     E item;
     Node<E> next;
     Node<E> prev;

     Node(Node<E> prev, E element, Node<E> next) {
         this.item = element;
         this.next = next;
         this.prev = prev;
     }
}

能夠看到,Node節點其實就是雙向鏈表,因爲是用鏈表實現,天然不用考慮擴容,只需對其修改時更新節點便可。LinkedList方法大可能是跟鏈表相關,選取addFirst分析一下:oop

public void addFirst(E e) {
    linkFirst(e);
}
private void linkFirst(E e) {
    final Node<E> f = first;//用f保存以前頭節點
    final Node<E> newNode = new Node<>(null, e, f);//以f做爲後置節點建立新節點
    first = newNode;//將新節點做爲頭節點
    if (f == null)
        last = newNode;
    else
        f.prev = newNode;//將新節點設爲f的前置節點
    size++;
    modCount++;
}

另外,LinkedList還有一些相似棧的操做函數:peek、pop、push(E)等。其餘方法與ArrayList大同小異。

3.Vector

Vector就是在ArrayList的基礎上增長了同步機制,對可能改變容器及內部元素的方法都加了同步鎖,Vector的加鎖機制是用Synchronized。這樣雖然安全且方便,但Synchronized是重量級鎖,同步塊在已進入的線程執行完以前會阻塞其餘線程的進入,Java的線程是映射到操做系統原生線程上的,若是要阻塞或喚醒一個線程,都須要操做系統從用戶態轉爲核心態,須要消耗不少處理器時間,甚至超過用戶代碼執行時間。這點須要注意!

4.Stack

Stack繼承自Vector,在此基礎上添加了一些棧操做,也是加了同步的。

3、Map系列

Map用於保存鍵值對,不管是HashMap,TreeMap仍是已棄用的HashTable或者線程安全的ConcurrentHashMap等,都是基於紅黑樹。咱們知道,JDK1.7之前的HashMap是基於數組加鏈表實現的,這樣通常狀況下能有不錯的查找效率,可是當hash衝突嚴重時,整個數組趨向一個單鏈表,這時查找的效率就會降低的很明顯,而紅黑樹經過其不錯的平衡性保證在hash衝突嚴重的狀況下仍然又不錯的查找效率。這裏優先介紹一下紅黑樹,具體實現會單獨介紹。

1.紅黑樹

紅黑樹是在普通二叉查找樹和AVL樹之間的平衡,既能保證不錯的平衡性,維護平衡的成本又比AVL低,定義以下:

  • 性質一:節點爲紅色或者黑色;
  • 性質二:根節點是黑色;
  • 性質三:每一個葉節點是黑色;
  • 性質四:每一個紅色節點的兩個子節點都是黑色;
  • 性質五:從任一節點到其沒個葉節點的全部路徑都包含相同數目的黑色節點

紅黑樹是Map系列容器的底層實現細節,關於具體的對紅黑樹的操做在Map的分析中會涉及。

2.HashMap

HashMap是很經常使用的Map結構,JDK1.7是由Entry數組實現,JDK1.8改成Node數組加TreeNode紅黑樹結合實現。

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
    TreeNode<K,V> parent;  // red-black tree links
    TreeNode<K,V> left;
    TreeNode<K,V> right;
    TreeNode<K,V> prev;    // needed to unlink next upon deletion
    boolean red;
    ...
}
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
    ...
}

transient Node<K,V>[] table;//Node數組
int threshold;//臨界值
final float loadFactor;//填充因子
transient int size;//元素個數

HashMap有四個重載的構造函數:

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

須要注意的是,傳入的initialCapacity並非實際的初始容量,HashMap經過tableSize函數將initialCapacity調整爲大於等於該值的最小2次冪

static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;//確保第一次出現1的位及其後一位都是1
    n |= n >>> 2;//確保前兩次出現的1及其後兩位都是1
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

以此類推,最後可以獲得一個2的冪。剛開始減1是爲了不當cap恰好等於2的整次冪時通過調整會變成原來的2倍。

HashMap可以擁有良好的性能很大程度依賴於它的擴容機制,從put方法放置元素開始分析整個擴容機制會比較清晰:

首先看一下hash函數,獲取key的hashCode,這個值與具體系統硬件有關,而後將hashCode值無符號右移16位後與原值異或獲得hash值,這實際上是簡化了JDK1.7的擾動函數。有興趣能夠看一下JDK1.7的擾動函數。擾動函數是爲了不hashCode只取後幾位時碰撞嚴重,由於咱們算數組的下標時是用(n-1)&hash,通常狀況下n不大,下標值就是hashCode的後幾位,這時擾動函數就能夠發揮做用。

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

咱們調用put函數往hashMap裏填充元素時,會調用putVal函數,會有如下幾種狀況:

  1. 數組爲空則新建數組;
  2. 判斷table[i]首個元素是否爲空,是則直接插入新節點;
  3. 若是對應位置存在節點,判斷首個元素是否和key同樣,是則覆蓋value;
  4. 若是首節點爲TreeNode,則直接在紅黑樹中插入鍵值對;
  5. 不然遍歷鏈表,若是存在節點與key相等,那麼退出循環,爲對應鍵賦值,不然在鏈表後添加一個節點,判斷鏈表長度是否超過臨界值,是則轉爲紅黑樹;
  6. 插入完成後判斷是否擴容。
public V put(K key, V value) {
    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)//數組爲空新建數組
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)//table[i]爲空直接插入新節點
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))//對應位置首個節點key相同
            e = p;
        else if (p instanceof TreeNode)//首節點爲TreeNode,直接在紅黑樹中插入
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {//遍歷鏈表,插入後判斷是否擴容
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                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;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

數組爲空時新建數組調用了resize方法,resize方法其實包括兩個部分,創建新數組和移動元素到新數組:

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;
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&//擴容兩倍
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) //只有臨界值時,將臨界值設爲數組容量
        newCap = oldThr;
    else {//不然使用默認值              
        newCap = DEFAULT_INITIAL_CAPACITY;
        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;
    //新建Node數組
        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 { // 將鏈表分爲兩部分總體位移至新數組
                    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 {
                        //新的下標值等於舊的下標值加oldCap的元素
                            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;
}

最後總體移動至新數組是JDk1.8對resize的優化。由於咱們每次擴容是原來容量的兩倍,那麼每次計算獲得的下標hash&(newCap-1)會有必定規律,由於newCap-1比oldCap多了一個高的1位,所以新的下標要麼等於舊的下標,要麼等於舊的下標加上oldCap,取決於hash值對應位是0仍是1,即e.hash&oldCap是0仍是1.

接下來看一下hashMap中的紅黑樹操做,hashMap中的紅黑樹操做仍是不少的,這裏以上面插入節點後鏈表長度超過8時轉爲紅黑樹調用的treeify方法爲例:

final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    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);
    }
}

treeify函數是一個雙層循環,外層循環從首節點開始遍歷全部節點,若是紅黑樹根節點爲空,將當前節點設爲根節點;不然進入內層循環,內層循環相似二叉查找樹的插入,經過比較hash值的大小逐層尋找新節點的插入位置,這裏有個細節須要注意:

//當節點的鍵沒有實現Comparable接口,或者兩個鍵經過conpareTo比較相等的時候,經過tieBreakOrder來比較大小,tieBreakOrder本質上比較hashCode。
else if ((kc == null &&(kc = comparableClassFor(k)) == null) ||
         (dir = compareComparables(kc, k, pk)) == 0)
    dir = tieBreakOrder(k, pk);

插入完成後會調用balanceInsertion來保證紅黑樹的平衡性:

static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
                                            TreeNode<K,V> x) {
    //新插入節點默認爲紅節點
    x.red = true;
    for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
        //x即爲根節點
        if ((xp = x.parent) == null) {
            x.red = false;
            return x;
        }
    //x爲根節點的子節點或者父節點爲黑節點
        else if (!xp.red || (xpp = xp.parent) == null)
            return root;
    //父節點爲祖父節點的左孩子
        if (xp == (xppl = xpp.left)) {
            //叔節點爲紅節點
            if ((xppr = xpp.right) != null && xppr.red) {
                //顏色轉換:祖父節點的兩個字節點由紅轉黑,祖父節點由黑轉紅
                xppr.red = false;
                xp.red = false;
                xpp.red = true;
                //繼續調整祖父節點
                x = xpp;
            }
            //叔節點爲黑節點
            else {
                //x節點爲父節點的右節點,左旋
                if (x == xp.right) {
                    root = rotateLeft(root, x = xp);
                    xpp = (xp = x.parent) == null ? null : xp.parent;
                }
                //右旋
                if (xp != null) {
                    xp.red = false;
                    if (xpp != null) {
                        xpp.red = true;
                        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);
                    }
                }
            }
        }
    }
}

相似操做再也不分析,其實就是紅黑樹的操做,包括插入,刪除。
最後分析一下keySet()這個方法:

public Set<K> keySet() {
    Set<K> ks = keySet;
    if (ks == null) {
        ks = new KeySet();
        keySet = ks;
    }
    return ks;
}

public final Iterator<K> iterator()     { return new KeyIterator(); }
final class KeyIterator extends HashIterator

能夠看到,當調用keySet的iterator()時,就持有了hashIterator,也就能夠訪問hashMap的內部數組,得到key的集合Set。

3.TreeMap

TreeMap是徹底基於紅黑樹的,並在此基礎上實現了NavigableMap接口。因此它的特色是可排序,該Map根據其鍵的天然順序(a.compareTo(b))進行排序,或者根據建立時提供的Comparator(comparactor.compare(a,b))進行排序,具體取決於使用的構造方法。

private final Comparator<? super K> comparator;
private transient Entry<K,V> root;
private transient int size = 0;
private transient int modCount = 0;
static final class Entry<K,V> implements Map.Entry<K,V> {
    K key;
    V value;
    Entry<K,V> left;
    Entry<K,V> right;
    Entry<K,V> parent;
    boolean color = BLACK;
    ...
}

能夠看到,TreeMap只有紅黑樹,且紅黑樹是經過內部類Entry來實現的。接下來重點查看一下put函數:

public V put(K key, V value) {
    Entry<K,V> t = root;
    if (t == null) {
        compare(key, key); // type (and possibly null) check

        root = new Entry<>(key, value, null);
        size = 1;
        modCount++;
        return null;
    }
    int cmp;
    Entry<K,V> parent;
    // split comparator and comparable paths
    Comparator<? super K> cpr = comparator;
    if (cpr != null) {
        do {
            parent = t;
            cmp = cpr.compare(key, t.key);
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
            else
                return t.setValue(value);
        } while (t != null);
    }
    else {
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
            Comparable<? super K> k = (Comparable<? super K>) key;
        do {
            parent = t;
            cmp = k.compareTo(t.key);
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
            else
                return t.setValue(value);
        } while (t != null);
    }
    Entry<K,V> e = new Entry<>(key, value, parent);
    if (cmp < 0)
        parent.left = e;
    else
        parent.right = e;
    fixAfterInsertion(e);
    size++;
    modCount++;
    return null;
}

能夠看到,先是判斷根結點的狀況,而後無非是根據是否有比較器分別討論,都是按二叉查找樹的規則插入。在插入完成以後再調用fixAfterInsertion,這個方法與HashMap的balanceInsertion基本相似。

TreeMap有一個構造函數是根據有序序列構建的:

public TreeMap(SortedMap<K, ? extends V> m) {
    comparator = m.comparator();
    try {
        buildFromSorted(m.size(), m.entrySet().iterator(), null, null);
    } catch (java.io.IOException cannotHappen) {
    } catch (ClassNotFoundException cannotHappen) {
    }
}

buildFromSorted經過重載方法完成紅黑樹的構建:

if (hi < lo) return null;

int mid = (lo + hi) >>> 1;

Entry<K,V> left  = null;
if (lo < mid)
    left = buildFromSorted(level+1, lo, mid - 1, redLevel,
                           it, str, defaultVal);

// extract key and/or value from iterator or stream
K key;
V value;
if (it != null) {
    if (defaultVal==null) {
        Map.Entry<?,?> entry = (Map.Entry<?,?>)it.next();
        key = (K)entry.getKey();
        value = (V)entry.getValue();
    } else {
        key = (K)it.next();
        value = defaultVal;
    }
} else { // use stream
    key = (K) str.readObject();
    value = (defaultVal != null ? defaultVal : (V) str.readObject());
}

Entry<K,V> middle =  new Entry<>(key, value, null);

// color nodes in non-full bottommost level red
if (level == redLevel)
    middle.color = RED;

if (left != null) {
    middle.left = left;
    left.parent = middle;
}

if (mid < hi) {
    Entry<K,V> right = buildFromSorted(level+1, mid+1, hi, redLevel,
                                       it, str, defaultVal);
    middle.right = right;
    right.parent = middle;
}

return middle;

從上面代碼能夠看出,首先以序列中間節點爲根節點,將序列分爲左右兩個部分,並分別創建成根節點的左右子樹,而後用一樣的方法,在子序列中尋找中間節點做爲子樹的根節點,以此遞歸下去。最終構建成一棵二叉樹。葉子節點以上是一棵滿二叉樹,而葉子節點則不必定,因此葉子節點都是紅節點,知足紅黑樹的性質。至於如何判斷葉子節點是經過節點的深度,首先經過computeRedLevel方法計算出葉子節點應該在的深度,而後每層遞歸深度加1,再判斷是否等於葉子深度,以此決定是否爲紅節點。

private static int computeRedLevel(int sz) {
    int level = 0;
    for (int m = sz - 1; m >= 0; m = m / 2 - 1)
        level++;
    return level;
}

4.LinkedHashMap

LinkedHashMap繼承自HashMap,它的內部類Entry也繼承自HashMap.Node。它重寫了一些HashMap的方法,在hashMap的基礎上,將全部元素連成一個雙向鏈表。

static class Entry<K,V> extends HashMap.Node<K,V> {
    Entry<K,V> before, after;
    Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}

transient LinkedHashMap.Entry<K,V> head;
transient LinkedHashMap.Entry<K,V> tail;
final boolean accessOrder;//在構造函數中初始化,用來指定迭代順序,true爲訪問順序,false爲插入順序

accessOrder是一個比較重要的標誌位,若是爲true,每次訪問元素都要及時調整鏈表。

5.HashTable

HashTable跟JDk1.7之前的HashMap同樣,是基於數組加鏈表的,不過它的方法都是同步的,HashTable效率很低,由於每次訪問修改都會對整個數組加鎖,咱們須要更細粒度的鎖以提升效率。ConcurrentHashMap相比而言擁有更高的效率,由於它不是對整個數組加鎖,這涉及到一些併發知識,具體的分析會在另一篇單獨展開。

4、Set系列

Set其實就是Map的鍵集合,查看源碼得知Set內部都保存着一個Map,對Set的訪問實際轉換爲對Map的訪問。

1.HashSet

HashSet經過內部的HashMap保存數據:

private transient HashMap<E,Object> map;
public HashSet() {
    map = new HashMap<>();
}

再來看一下HashSet的幾個方法:

public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}
public int size() {
    return map.size();
}
public Iterator<E> iterator() {
    return map.keySet().iterator();
}

能夠看到,HashSet的方法都是訪問內部的map,而鍵值對的值都是PRESENT,只有鍵是有意義的。

2.TreeSet

TreeSet內部經過NavigableMap的鍵保存數據,方法也都是轉爲對map的操做,再也不詳述。

5、PriorityQueue

Queue接口也繼承Collection,並且Priority跟上一篇關於簡單算法的博客中介紹的堆關係密切,因此藉助分析優先隊列複習一下堆的知識。Priority也是用動態數組存儲元素的,以下:

transient Object[] queue;
private int size = 0;
private final Comparator<? super E> comparator;
//擴容函數
private void grow(int minCapacity) {
    int oldCapacity = queue.length;
    // Double size if small; else grow by 50%
    int newCapacity = oldCapacity + ((oldCapacity < 64) ?
                                     (oldCapacity + 2) :
                                     (oldCapacity >> 1));
    // overflow-conscious code
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    queue = Arrays.copyOf(queue, newCapacity);
}

接下來是添加元素:

public boolean add(E e) {
    return offer(e);
}
public boolean offer(E e) {
    if (e == null)
        throw new NullPointerException();
    modCount++;
    int i = size;
    if (i >= queue.length)
        grow(i + 1);
    size = i + 1;
    if (i == 0)
        queue[0] = e;
    else
        siftUp(i, e);
    return true;
}
private void siftUp(int k, E x) {
    if (comparator != null)
        siftUpUsingComparator(k, x);
    else
        siftUpComparable(k, x);
}

@SuppressWarnings("unchecked")
private void siftUpComparable(int k, E x) {
    Comparable<? super E> key = (Comparable<? super E>) x;
    while (k > 0) {
        int parent = (k - 1) >>> 1;
        Object e = queue[parent];
        if (key.compareTo((E) e) >= 0)
            break;
        queue[k] = e;
        k = parent;
    }
    queue[k] = key;
}

add方法經過offer方法調用siftUp,siftUp根據是否有定義的comparator進行區分按不一樣的排序方式調整最小堆。兩種方式代碼同樣,以siftUpComparable爲例,重新插入位置的父親開始比較,若父節點小於子節點退出循環,將帶插入元素放置在初始位置;不然將父節點元素移動到子節點上,再用祖父節點比較,一直找到合適位置放置新增元素爲止。

再看一下與之對應的方法SiftDownComparable:

private void siftDownComparable(int k, E x) {
    Comparable<? super E> key = (Comparable<? super E>)x;
    int half = size >>> 1;        // loop while a non-leaf
    while (k < half) {
        int child = (k << 1) + 1; // assume left child is least
        Object c = queue[child];
        int right = child + 1;
        if (right < size &&
            ((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)
            c = queue[child = right];
        if (key.compareTo((E) c) <= 0)
            break;
        queue[k] = c;
        k = child;
    }
    queue[k] = key;
}

這是從上到下的調整堆,與上面恰好相反,首先從兩個孩子中找出較小的那個,再與待插入比較,若是待插入元素小於較小子元素,那麼知足最小堆,直接退出循環並未當前位置賦值。不然將子元素移動到父元素上,在那當前元素與子元素的子元素比較下去一直到找到待插入元素的合適位置。

最後分析一下刪除元素的方法:

public boolean remove(Object o) {
    int i = indexOf(o);
    if (i == -1)
        return false;
    else {
        removeAt(i);
        return true;
    }
}
private E removeAt(int i) {
    // assert i >= 0 && i < size;
    modCount++;
    int s = --size;
    if (s == i) // removed last element
        queue[i] = null;
    else {
        E moved = (E) queue[s];
        queue[s] = null;
        siftDown(i, moved);
        if (queue[i] == moved) {
            siftUp(i, moved);
            if (queue[i] != moved)
                return moved;
        }
    }
    return null;
}

若是刪除索引不是最後一個位置,那麼獲取最後一個節點的值並刪除節點,而後用最後一個節點的值覆蓋待刪除位置節點的值並調整結構,若調整完成以後結構未發生改變則須要繼續向上調整,若是已經向下調整過了(結構發生了改變),那麼無需再調整了。

總結

還有部分容器沒有列出,關於併發部分的,例如ConcurrentHashMap會在介紹concurrent包時一併介紹。

相關文章
相關標籤/搜索