以下内容皆基于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的基本实现:
- 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)。 - ArrayDeque
以循环数组实现,维持两个指向队头和队尾的索引。数组默认起始大小为16,双倍扩容,即数组大小永远为2的幂。不允许null元素。
大多数方法为O(1);remove(Object)和contains(Object)为O(n)。
List的基本实现:
- ArrayList
数组实现的列表,允许null值。默认起始大小为10,每次扩容增加50%。
size()、isEmpty()、get(i)、set(i)、add(Object)为O(1);其他操作为O(n)(粗略的说)。 - LinkedList
双向链表实现的列表。允许null值。同时实现了Deque接口。
在链表两头的操作耗时O(1);get(i)、set(i)等操作为O(n/2)(i>n/2时会从后往前遍历)。
SET的基本实现:
- HashSet
内部是HashMap,value都用同一个假值。允许null值。因为依赖HashMap实现,所以性能同HashMap相仿。 - LinkedHashSet
内部是LinkedHashMap。遍历时按照元素插入的顺序遍历(一个元素的插入顺序不受重新插入的影响)。 - TreeSet
内部是TreeMap。实现了NavigableSet。支持遍历时按元素的大小遍历。当没有提供Comparator时,元素必须实现Comparable接口。性能不如HashSet,add、remove、contains复杂度为O(log(n))。
Map的基本实现:
- 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之间),则会重新将树变为链表。
- LinkedHashMap
拓展HashMap实现,提供按插入顺序遍历的功能(如果accessOrder为true则是按访问顺序)。Entry增加了before、after两个指针,以实现双向链表;同时维护指向表头和表尾的指针。依赖HashMap的afterNodeAccess()、afterNodeRemoval()、linkNodeLast()(由newNode()、newTreeNode()调用)三个方法维护链表顺序。
可利用accessOrder和[removeEldestEntry(Map.Entry)方法实现一个简单的LRU缓存。 - 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 super K> 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 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 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