Java Collections Framework概览 Part1

以下内容皆基于JDK1.8。
J.C.F的根接口是Collection,其下直接继承Collection的接口主要为Queue、List、Set和Map。
然后Queue又分为Deque、BlockingQueue、BlockingDeque、TransferQueue;
Set分为:SortedSet、NavigableSet(为什么没有ConcurrentSet这个接口呢?奇怪);
Map分为:SortedMap、NavigableMap、ConcurrentMap;
下面从基本实现讲起:
Queue的基本实现

  1. PriorityQueue
    基于平衡二叉堆实现,节点存放在数组中。位置为i的节点的父节点的位置为[k/2],而它的两个子节点的位置分别为2i和2i+1。当没有提供Comparator时,元素必须实现Comparable接口。注意虽然取元素时是有序的,但是用iterator遍历时元素无序。不允许null元素。扩容时当元素个数小于64时增加一倍,超过64增加50%。
    查看队头元素和队列大小为O(1);入队和出队为O(log(n));remove(Object)和contains(Object)则为O(n)。
  2. ArrayDeque
    以循环数组实现,维持两个指向队头和队尾的索引。数组默认起始大小为16,双倍扩容,即数组大小永远为2的幂。不允许null元素。
    大多数方法为O(1);remove(Object)和contains(Object)为O(n)。

List的基本实现

  1. ArrayList
    数组实现的列表,允许null值。默认起始大小为10,每次扩容增加50%。
    size()、isEmpty()、get(i)、set(i)、add(Object)为O(1);其他操作为O(n)(粗略的说)。
  2. LinkedList
    双向链表实现的列表。允许null值。同时实现了Deque接口。
    在链表两头的操作耗时O(1);get(i)、set(i)等操作为O(n/2)(i>n/2时会从后往前遍历)。

SET的基本实现

  1. HashSet
    内部是HashMap,value都用同一个假值。允许null值。因为依赖HashMap实现,所以性能同HashMap相仿。
  2. LinkedHashSet
    内部是LinkedHashMap。遍历时按照元素插入的顺序遍历(一个元素的插入顺序不受重新插入的影响)。
  3. TreeSet
    内部是TreeMap。实现了NavigableSet。支持遍历时按元素的大小遍历。当没有提供Comparator时,元素必须实现Comparable接口。性能不如HashSet,add、remove、contains复杂度为O(log(n))。

Map的基本实现

  1. HashMap
    采用桶数组存放元素。允许null键和null值。get返回null不代表key不存在,可能value就是null,可以用containsKey方法辨别。
    默认初始桶数量为16,扩容时乘以2。计算下标时用(n-1)&hash(n为桶的数量)。
    JDK1.8里,当一个桶里的Entry数超过8时,将桶由链表结构改为红黑树结构。
    (1). 无论在get还是put时都会对键进行重新hash,hash()方法如下:
static final int hash(Object key) {    
    int h;    
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

将Key的hashcode值的高16位与低16位做异或可以利用到高位,减少碰撞。同时计算开销也很小。
(2). get(Object key) 调用getNode(),代码如下:

final Node getNode(int hash, Object key) {    
    Node[] tab; Node first, e; int n; K k;    
    if ((tab = table) != null && (n = tab.length) > 0 &&  (first = tab[(n - 1) & hash]) != null) {        
        if (first.hash == hash && // 检查是否直接命中            
            ((k = first.key) == key || (key != null && key.equals(k))))            
                return first;        
        if ((e = first.next) != null) {
            //若第一个节点为TreeNode            
            if (first instanceof TreeNode)                
                return ((TreeNode)first).getTreeNode(hash, key); 
            // 否则为链表结构           
            do {                
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))                    
                    return e;            
            } while ((e = e.next) != null);        
        }    
    }    
    return null;
}

大致思路如下:
对key的hashCode()做hash,然后再计算index;
对第index个桶里的第一个节点,检查是否直接命中;
如果没有直接命中,则在此桶的后续节点中继续查找;
若第一个节点为TreeNode,代表此桶为红黑树结构,复杂度为O(logn); 否则为链表结构,复杂度O(n)。
(3). put调用putVal,代码如下:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    Node[] tab; Node p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    //如果第index个桶是空的,则直接赋值
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node e; K k;
        //如果第一个节点的key等于要放入的键值对的key
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)//如果第一个节点是TreeNode
             e = ((TreeNode)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) { // 如果是已有的映射
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    //如果桶数组快满了
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

大致思路如下:
如果桶数组为null或长度为零,初始化桶数组。
如果第index个桶是空的,则直接赋值;
否则,先检查第一个节点,共有三种情况:
如果第一个节点的key等于要放入的键值对的key,则节点已找到;
如果第一个节点是TreeNode,说明此桶为红黑树结构,调用红黑树的put方法;
否则,在链表的最后放入新键值对对应的节点或者找到已有的映射节点,并且当此桶的节点数大于8时,要将此桶红黑树化。
在以上三种情况中,如果是已有的映射,则将value换为新值,并且返回旧值。
最后,如果桶数组快满了(超过threshold = load factor*current capacity),就要扩容。
(4). 红黑树化的过程:
使用哈希值排序,哈希值较大的会插到右子树里。
如果哈希值相等,key最好是实现Comparable接口,这样就可以按照顺序来进行插入。
当因为删除或扩容导致桶里的数目过少(依树结构而定,在2~6之间),则会重新将树变为链表。

  1. LinkedHashMap
    拓展HashMap实现,提供按插入顺序遍历的功能(如果accessOrder为true则是按访问顺序)。Entry增加了before、after两个指针,以实现双向链表;同时维护指向表头和表尾的指针。依赖HashMap的afterNodeAccess()、afterNodeRemoval()、linkNodeLast()(由newNode()、newTreeNode()调用)三个方法维护链表顺序。
    可利用accessOrder和[removeEldestEntry(Map.Entry)方法实现一个简单的LRU缓存。
  2. TreeMap
    以红黑树实现的NavigableMap,维护一个指向根节点的指针。Entry按键的顺序排列。当没有提供Comparator时,键值必须实现Comparable接口。不支持键为null。 containsKey()、get()、put()和remove()复杂度为O(log(n))。
    (1). get()方法很简单,和普通二叉查找树的查找过程一样。
    (2). put方法代码如下:
public V put(K key, V value) {
    Entry 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 parent;
    // split comparator and comparable paths
    Comparator cpr = comparator;
    //如果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 k = (Comparable) 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 e = new Entry<>(key, value, parent);
    if (cmp < 0)
        parent.left = e;
    else
        parent.right = e;
    fixAfterInsertion(e);
    size++;
    modCount++;
    return null;
}

大致思路为:如果key存在的话,旧值被替换;如果key不存在的话,则添加新节点,然后平衡红黑树。
(3)TreeMap的遍历是通过调用entrySet().iterator()返回的PrivateEntryIterator的子类EntryIterator进行的,核心方法是successor(Entry t),代码如下:

static  TreeMap.Entry successor(Entry t) {
    //空节点,没有后继节点
    if (t == null)
        return null;
    else if (t.right != null) {
        //右子树不为空,后继节点是右子树的最左节点
        Entry p = t.right;
        while (p.left != null)
            p = p.left;
        return p;
    } else {
        //右子树为空,后继节点为该节点所在左子树的第一个祖先节点
        Entry p = t.parent;
        Entry ch = t;
        while (p != null && ch == p.right) {
            ch = p;
            p = p.parent;
        }
        return p;
    }
}

寻找一个节点的后继节点的算法为:
a. 空节点,没有后继节点;
b. 有右子树的节点,后继节点是右子树的最左节点;
c. 无右子树的节点,后继节点是该节点所在左子树的第一个祖先节点。

Java Collections Framework概览的Part1就到这里。Part2会写J.U.C包里的并发容器。内容参考了一些网上的博文,还保留地址的我会在下面列出。感谢各种博主的分享。

参考:
1.《关于Java集合的小抄》http://calvin1978.blogcn.com/articles/collection.html

你可能感兴趣的:(Java Collections Framework概览 Part1)