一、总结
Java 集合在util([jutil])包下,主要包括Collection和Map两个接口。
Collection接口没有直接的实现类,其下包括Set,List,Queue三个接口。
Map接口是Java.util包中的另一个接口,其中包括了Hashtable、HashMap、LinkedHashMap、TreeMap、ConcurrentHashMap等。
Set中的元素是无序,不可重复的。Set中包含:AbstractSet,HashSet,TreeSet,Set接口底层实现是根据Map的,采用适配器模式。add方法即Map接口中的put(e,null)。 下面详解。
List中元素是有序,可重复的。List中的元素查询速度快,插入,删除慢。List中包含:ArrayList,LinkedList,Vector(Stack 继承了Vector)等实现类。
Queue即队列,其中主要实现类有 LinkedList,优先级队列 :PriorityQueue
二、各具体集合类的简介及联系
1.ArrayList
。删除的时间复杂度为。有两个标志数据,capacity为容量,size为当前元素的数量。当容量不够扩容时使用Arrays.copyOf进行复制操作,并且容量扩展为原来容量的1.5倍。DEFULT_CAPACITY
为10。copyOf 方法调用了System.arraycopy方法,该方法是一个native方法。
我们可以先看一下add() 方法:
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
其中每次添加元素之前都要执行一下ensureCapacityInternal()方法,我们来看一下:
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); //扩容1.5倍
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);
}
首先来看Arrays.copyof()方法。它有很多个重载的方法,但实现思路都是一样的,我们来看泛型版本的源码:
public static <T> T[] copyOf(T[] original, int newLength) {
return (T[]) copyOf(original, newLength, original.getClass());
}
很明显调用了另一个copyof方法,该方法有三个参数,最后一个参数指明要转换的数据的类型,其源码如下:
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
这里可以很明显地看出,该方法实际上是在其内部又创建了一个长度为newlength的数组,调用System.arraycopy()方法,将原来数组中的元素复制到了新的数组中。
下面来看System.arraycopy()方法。该方法被标记了native,调用了系统的C/C++代码,在JDK中是看不到的,但在openJDK中可以看到其源码。该函数实际上最终调用了C语言的memmove()函数,因此它可以保证同一个数组内元素的正确复制和移动,比一般的复制方法的实现效率要高很多,很适合用来批量处理数组。Java强烈推荐在复制大量数组元素时用该方法,以取得更高的效率。
具体可见:ArrayList实现原理
2.Vector
Collections.sychronizedList()
包装ArrayList)单线程情况下效率较低,同时在add操作时,ArrayList扩容为1.5倍,Vector扩容为2倍。
3.LinkedList(1.8改动很大,需要补充)
时间复杂度,添加、删除需要的时间复杂度。
可见这篇:LinkedList源码解析
JDK1.8中与之前有所不同:
私有属性:
transient int size = 0;
transient Node<E> first;
transient Node<E> last;
其中first指向第一个节点,last指向最后一个节点,Node为节点类:
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;
}
}
节点类很简单,item存放数据,previous与next分别存放前后节点的信息(在数据结构中我们通常称之为前后节点的指针)。
4.HashTable(需补充)
Hashtable是一个线程安全的类,其方法都是sychronized的。其与HashMap的关系有点像Vector与ArrayList的关系。
Hashtable是遗留类,很多映射的常用功能与HashMap类似,不同的是它承自Dictionary类,并且是线程安全的,任一时间只有一个线程能写Hashtable,并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁。Hashtable不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换。
5.HashMap(重点)
HashMap继承了AbstractMap类,实现了Map<K,V>, Cloneable, Serializable接口。
HashMap根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 HashMap最多只允许一条记录的键为null,允许多条记录的值为null。HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap。
5.1 HashMap的底层实现 参考:HashMap(JDK1.8)
在JDK1.8中,HashMap是数组+链表+红黑树实现的,如下图所示。
在理解Hash和扩容流程之前,我们得先了解下HashMap的几个字段。从HashMap的默认构造函数源码可知,构造函数就是对下面几个字段进行初始化,源码如下:
1
2
3
4
|
int
threshold;
// 所能容纳的key-value对极限
final
float
loadFactor;
// 负载因子
int
modCount;
int
size;
|
首先,Node[] table的初始化长度length(默认值是16),Load factor为负载因子(默认值是0.75),threshold是HashMap所能容纳的最大数据量的Node(键值对)个数。threshold = length * Load factor。也就是说,在数组定义好长度之后,负载因子越大,所能容纳的键值对个数越多。
5.2 HashMap中的put方法
①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;
②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;
③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;
④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤;
⑤.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;
⑥.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。
JDK1.8HashMap的put方法源码如下:
1
public
V put(K key, V value) {
2
// 对key的hashCode()做hash
3
return
putVal(hash(key), key, value,
false
,
true
);
4
}
5
6
final
V putVal(
int
hash, K key, V value,
boolean
onlyIfAbsent,
7
boolean
evict) {
8
Node<K,V>[] tab; Node<K,V> p;
int
n, i;
9
// 步骤①:tab为空则创建
10
if
((tab = table) ==
null
|| (n = tab.length) ==
0
)
11
n = (tab = resize()).length;
12
// 步骤②:计算index,并对null做处理
13
if
((p = tab[i = (n -
1
) & hash]) ==
null
)
14
tab[i] = newNode(hash, key, value,
null
);
15
else
{
16
Node<K,V> e; K k;
17
// 步骤③:节点key存在,直接覆盖value
18
if
(p.hash == hash &&
19
((k = p.key) == key || (key !=
null
&& key.equals(k))))
20
e = p;
21
// 步骤④:判断该链为红黑树
22
else
if
(p
instanceof
TreeNode)
23
e = ((TreeNode<K,V>)p).putTreeVal(
this
, tab, hash, key, value);
24
// 步骤⑤:该链为链表
25
else
{
26
for
(
int
binCount =
0
; ; ++binCount) {
27
if
((e = p.next) ==
null
) {
28
p.next = newNode(hash, key,value,
null
);
//链表长度大于8转换为红黑树进行处理
29
if
(binCount >= TREEIFY_THRESHOLD -
1
)
// -1 for 1st
30
treeifyBin(tab, hash);
31
break
;
32
}
// key已经存在直接覆盖value
33
if
(e.hash == hash &&
34
((k = e.key) == key || (key !=
null
&& key.equals(k))))
break
;
36
p = e;
37
}
38
}
39
40
if
(e !=
null
) {
// existing mapping for key
41
V oldValue = e.value;
42
if
(!onlyIfAbsent || oldValue ==
null
)
43
e.value = value;
44
afterNodeAccess(e);
45
return
oldValue;
46
}
47
}
48
++modCount;
49
// 步骤⑥:超过最大容量 就扩容
50
if
(++size > threshold)
51
resize();
52
afterNodeInsertion(evict);
53
return
null
;
54
}
|