JDK容器

前言

阅读JDK源码有段时间了,准备以博客的形式记录下来,也方便复习时查阅,本文参考JDK1.8源码。

一、Collection

Collection是所有容器的基类,定义了一些基础方法。List、Set、Map、Queue等子接口都继承于它,并根据各自特性添加了额外的方法。

二、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是为了并行遍历,会在后面统一分析。

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分析一下:

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,在此基础上添加了一些栈操作,也是加了同步的。

三、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相比而言拥有更高的效率,因为它不是对整个数组加锁,这涉及到一些并发知识,具体的分析会在另外一篇单独展开。

四、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的操作,不再详述。

五、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包时一并介绍。


只吃全麦面包的人
28 声望2 粉丝